29 Commits

Author SHA1 Message Date
de69397aa5 start rewrite 2026-01-15 12:23:13 -05:00
2c2dc57453 WIP broken stage 2026-01-06 15:40:30 -05:00
85bd564164 release 0.3.0 2026-01-04 16:27:49 -05:00
0a0b1fa8c3 large changes 2026-01-04 16:27:39 -05:00
608a083861 patch: fix the back option for archives 2026-01-03 16:51:16 -05:00
50a45b6a82 update archive example 2026-01-03 16:50:33 -05:00
19752a0c89 change logging format 2026-01-03 09:53:07 -05:00
5192919645 update gitignore 2026-01-02 10:49:04 -05:00
f763f57001 rewrite Dockerfile 2026-01-02 10:06:27 -05:00
629fd06be0 exit beta 2026-01-01 23:06:30 -05:00
bedcfe781d updated logging 2026-01-01 23:05:29 -05:00
9364df2645 new year bump 2025-12-31 13:09:37 -05:00
4eaead6abc hint for running project 2025-12-31 12:16:59 -05:00
4abf2969ca new hint ui function 2025-12-31 12:16:51 -05:00
1c229f1b3e update examples 2025-12-28 20:17:46 -05:00
5a733b8642 create default site 2025-12-28 17:07:08 -05:00
11ab1630be maint: annotate source code 2025-12-28 16:39:33 -05:00
5fabd0233d fix: print usage to err instead of out 2025-12-28 16:38:40 -05:00
c43e905729 Merge branch 'new-default-project-creation-structure' 2025-12-28 15:52:26 -05:00
3430141184 new creation format 2025-12-28 15:51:58 -05:00
c5fe2eb7e7 maint: tidy 2025-12-28 14:08:55 -05:00
afd0d9eef4 doc: fix update date 2025-12-27 19:17:19 -05:00
b593aa26f0 doc: update documentation 2025-12-27 19:16:59 -05:00
fb8dc3cb90 fix: use 0.0.0.0 instead of 127.0.0.0 2025-12-27 16:29:55 -05:00
c681e342a0 fix: update docker interface 2025-12-27 12:04:04 -05:00
99e437b42b Docker image first version 2025-12-26 22:03:49 -05:00
56f22bb472 limit philosphy to 80 width 2025-12-26 15:00:27 -05:00
e53cc17025 default stylua formatting 2025-12-26 13:24:30 -05:00
2798cd6553 Update README.md 2025-12-26 10:16:42 -05:00
54 changed files with 1403 additions and 1283 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
fes
*.tar.gz
/stuff/

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2025 fSD
Copyright (c) 2025-2026 fSD
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -12,4 +12,4 @@ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
PERFORMANCE OF THIS SOFTWARE.

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache git build-base
COPY . .
RUN make
FROM alpine:3.19
COPY --from=builder /src/fes /usr/local/bin/fes
WORKDIR /app
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/fes"]
CMD ["run", "/app"]

View File

@@ -34,12 +34,12 @@ go install fes
## Documentation
Run `fes run doc` for the documentation website or goto [docs.vxserver.dev](https://docs.vxserver.dev)
Run `fes doc` for the documentation website or goto [docs.vxserver.dev](https://docs.vxserver.dev)
## License
ISC License
Copyright (C) 2025 fSD
Copyright (C) 2025-2026 fSD
See `COPYING`

View File

@@ -0,0 +1,5 @@
# simple
This simple example shows the extensibility of the Fes framework. It shows the
you do not necessarily need to use the site object (although it is recommended)
you can define your own site, similar to how Lisps do things.

View File

@@ -0,0 +1,4 @@
# hello
This is a very simple hello world program, the only difference between this and
default is this README.

View File

@@ -1,5 +1,5 @@
[app]
name = "advanced"
name = "default"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -0,0 +1,33 @@
# default
```
fes new default
```
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
```
.
├── Fes.toml
├── README.md
└── www
└── index.lua
```
Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `fes run .` | Runs the project at `.` |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).

View File

@@ -0,0 +1,8 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!")
return site

View File

@@ -0,0 +1,6 @@
# error
This shows what a Lua error looks like to the user. Lua errors are the most
common and the most critical so that is why they are shown to the user. Other,
lesser errors, are only shown to the developer because of their different
nature.

View File

@@ -1,15 +1,10 @@
local fes = require("fes")
local std = fes.std
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
This is what an error looks like
site:h1("Hello, World!")
site:note(
fes.app.foo.render()
)
return site

View File

@@ -0,0 +1,3 @@
# markdown
This example demonstrate Fes's ability to handle markdown routes.

View File

@@ -0,0 +1,3 @@
# Markdown!
**Fes** also supports markdown routes!

View File

@@ -0,0 +1,5 @@
[app]
name = "best"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -0,0 +1,23 @@
# best
This is an example of best practices for the Fes framework.
## Parts
With best practice we can break our sites into a few parts.
## Index
The main page of the site loads in the header and the footer, as well as shows
some core information
## Include
Within include the header and footer are defined.
* **Header:** Site navigation and name display
* **Footer:** Extra and external information.
## Static
This is where we store our favicon.

View File

@@ -0,0 +1,13 @@
local footer = {}
footer.render = function(std)
return table.concat({
std.h2("Other resources"),
std.tl({
std.external("https://git.vxserver.dev/fSD/fes", "Fes source"),
std.external("https://docs.vxserver.dev/static/fes.html", "Documentation"),
}),
})
end
return footer

View File

@@ -0,0 +1,7 @@
local header = {}
header.render = function(std)
return std.center(std.ha("/", std.h1("Best Practices")))
end
return header

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,20 @@
local fes = require("fes")
local std = fes.std
local u = fes.util
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site.title = "Best practices"
site.favicon = "/static/favicon.ico"
site:banner(fes.app.header.render(std))
site:note(u.cc {
std.h2("Hello, World!"),
std.p("This is an example of the best practices/canonical Fes site.")
})
site:note(fes.app.footer.render(std))
return site

View File

@@ -0,0 +1,4 @@
# archive
This example demonstrates the archive feature of Fes it is useful for file
sharing purposes.

View File

@@ -0,0 +1,22 @@
Pinnipeds [2] are the seals and their relatives, a group of semi-aquatic marine
mammals. The Pinnipedia is in the Order Carnivora. There are three seal
families: Odobenidae (walruses), Otariidae (eared seals, including sea lions
and fur seals), and Phocidae (true seals).[3]
Seals are sleek-bodied and barrel-shaped. Their bodies are well adapted to the
aquatic habitat where they spend most of their lives. Pinnipeds have flippers
for hands, big bulky bodies, doggish faces, and big eyes. Unlike cetaceans,
pinnipeds have their noses on their faces, and each nostril of the nose closes
when the pinniped goes underwater. Like cetaceans, pinnipeds have a thick layer
of blubber (fat) just under their skin: this blubber keeps them warm in cold
waters and keeps them fed during times when food is not easily found. When they
cannot find food, they live off the fat in the blubber.
Pinnipeds are carnivorous. This means they eat only meat (such as fish or
squid) and not plants. However, almost all pinnipeds can be eaten by polar
bears, sharks and killer whales.
Seals are often trained in zoos or aquariums to put on shows. However, in
Sweden, it is illegal to train a seal to balance a ball on its nose.[4]
From [Pinniped Wikipedia](https://simple.wikipedia.org/wiki/Pinniped)

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,17 +1,10 @@
local fes = require("fes")
local std = fes.std
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!")
site:note(fes.util.cc {
std.h2("Files"),
std.ul {
std.a("/archive", "to the file room!"),
}
})
site:a("/archive", fes.std.h2("To the file room!"))
return site

View File

@@ -0,0 +1,5 @@
[app]
name = "extentions"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -0,0 +1,33 @@
# extentions
```
fes new extentions
```
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
```
.
├── Fes.toml
├── README.md
└── www
└── index.lua
```
Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `fes run .` | Runs the project at `.` |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).

View File

@@ -0,0 +1,12 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev/", "fSD")
site:extend("myext", {
shout = function(self, str) return self:g(str:upper()) end
})
site.myext:shout("hello world")
return site

View File

@@ -1,7 +0,0 @@
local foo = {}
foo.render = function()
return "This was called from a foo function"
end
return foo

View File

@@ -1,6 +0,0 @@
local fes = require("fes")
local site = fes.fes()
This is what an error looks like
return site

View File

@@ -1 +0,0 @@
# Hello, World!

14
go.mod
View File

@@ -3,13 +3,15 @@ module fes
go 1.25.4
require (
github.com/fatih/color v1.18.0 // indirect
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect
github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585 // indirect
github.com/fatih/color v1.18.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/yuin/gopher-lua v1.1.1
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/sys v0.25.0 // indirect
)

2
go.sum
View File

@@ -1,9 +1,7 @@
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585/go.mod h1:6grYm5/uY15CwgBBqwA3+o/cAzaxssckznJ0B35ouBY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

View File

@@ -183,6 +183,7 @@ footer {
<li><a href="#introduction">Introduction</a></li>
<li><a href="#installation">Installation</a></li>
<li><a href="#usage">Usage</a></li>
<li><a href="#quick">Quick Start</a></li>
<li><a href="#cli-reference">Cli Reference</a></li>
<li><a href="#reference">Reference</a></li>
</ul>
@@ -210,6 +211,34 @@ footer {
</ul>
</section>
<section id="quick">
<h2>Quick Start</h2>
<pre><code>fes new hello</code></pre>
<p>This creates a new project under the name <code>hello</code>.</p>
<pre><code>fes run hello</code></pre>
<p>This runs your project <code>hello</code>, by default at <a href="localhost:3000" target="_blank">localhost:3000</a>.</p>
<h3>Extensions</h3>
<p>Let's add a paragraph to this simple site. Right now you have the following page:</p>
<pre><code>local fes = require("fes")
local site = fes.fes()
-- site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "vx-clutch")
site:h1("Hello, World!")
return site</code></pre>
<p>To add a simple paragraph modify like so:</p>
<pre><code>local fes = require("fes")
local site = fes.fes()
-- site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "vx-clutch")
site:h1("Hello, World!")
site:p("This is a paragraph")
return site</code></pre>
</section>
<section id="cli-reference">
<h2>Cli Reference</h2>
<table> <thead>
@@ -219,18 +248,6 @@ footer {
</tr>
</thead>
<tbody>
<tr>
<td><code>--help</code></td>
<td>Display help information</td>
</tr>
<tr>
<td><code>--no-color</code></td>
<td>Disable color output</td>
</tr>
<tr>
<td><code>-p &lt;port&gt;</code></td>
<td>Set the server port</td>
</tr>
<tr>
<td><code>new &lt;project&gt;</code></td>
<td>Create a new projet called &lt;project&gt;</td>
@@ -243,6 +260,30 @@ footer {
<td><code>run &lt;project&gt;</code></td>
<td>Run the projet called &lt;project&gt;</td>
</tr>
<tr>
<td><code>-help</code></td>
<td>Display help information.</td>
</tr>
<tr>
<td><code>-V1</code></td>
<td>Print extended version information, this is very helpful when it comes to bug reporting.</td>
</tr>
<tr>
<td><code>-no-color</code></td>
<td>Disable color output.</td>
</tr>
<tr>
<td><code>-p &lt;port&gt;</code></td>
<td>Set the server port.</td>
</tr>
<tr>
<td><code>-static</code></td>
<td>Render and save all pages. (this feature is yet to be implemented)</td>
</tr>
<tr>
<td><code>-version</code></td>
<td>Print the version.</td>
</tr>
</tbody>
</table>
</section>
@@ -586,7 +627,7 @@ return hello</pre></code> This can be called from another with,
</section>
<footer>
<p>Last updated: 2025-12-16</p>
<p>Last updated: 2025-12-27</p>
</footer>
</main>
</body>

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,30 @@
local std = require("lib.std")
local symbol = require("lib.symbol")
local M = {}
M.__index = M
function M.fes(header, footer)
local config = {} local site_config = {}
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config then
config = fes_mod.config
if config.site then
site_config = config.site
end
end
local config = {}
local site_config = {}
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config then
config = fes_mod.config
if config.site then
site_config = config.site
end
end
if site_config.favicon then
site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">'
end
if site_config.favicon then
site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">'
end
local self = {
version = site_config.version,
title = site_config.title,
copyright = site_config.copyright,
favicon = site_config.favicon,
header = header or [[
local self = {
version = site_config.version,
title = site_config.title,
copyright = site_config.copyright,
favicon = site_config.favicon,
header = header or [[
<!DOCTYPE html>
<html lang="en">
<head>
@@ -300,7 +302,7 @@ em, i { font-style: italic; }
<body>
<div class="container">
]],
footer = footer or [[
footer = footer or [[
<footer class="footer">
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
<a href="https://www.lua.org/" target="_blank">Lua Powered</a>
@@ -311,38 +313,63 @@ em, i { font-style: italic; }
</body>
</html>
]],
parts = {}
}
parts = {},
}
return setmetatable(self, M)
return setmetatable(self, M)
end
function M:custom(str)
table.insert(self.parts, str)
return self
function M:g(str)
table.insert(self.parts, str)
return self
end
function M:extend(name, tbl)
if type(name) ~= "string" then
error("First argument to extend must be a string (namespace name)")
end
if type(tbl) ~= "table" then
error("Second argument to extend must be a table of functions")
end
self[name] = {}
for k, v in pairs(tbl) do
if type(v) ~= "function" then
error("Extension values must be functions, got " .. type(v) .. " for key " .. k)
end
self[name][k] = function(...)
return v(self, ...)
end
end
return self
end
for name, func in pairs(std) do
if type(func) == "function" then
M[name] = function(self, ...)
local result = func(...)
table.insert(self.parts, result)
return self
end
end
if type(func) == "function" then
M[name] = function(self, ...)
local result = func(...)
table.insert(self.parts, result)
return self
end
end
end
function M:build()
local header = self.header
header = header:gsub("{{TITLE}}", self.title or "Document")
local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">')
header = header:gsub("{{FAVICON}}", favicon_html or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]])
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
local header = self.header
header = header:gsub("{{TITLE}}", self.title or "Document")
local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">')
header = header:gsub(
"{{FAVICON}}",
favicon_html
or
[[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]
)
local footer = self.footer:gsub("{{COPYRIGHT}}",
self.copyright or symbol.legal.copyright .. "The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
end
M.__tostring = function(self)
return self:build()
return self:build()
end
return M

27
lib/site.lua Normal file
View File

@@ -0,0 +1,27 @@
local M = {}
function M.name()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.name then
return fes_mod.config.site.name
end
return ""
end
function M.version()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.version then
return fes_mod.config.site.version
end
return ""
end
function M.authors()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.authors then
return fes_mod.config.site.authors
end
return {}
end
return M

View File

@@ -1,154 +1,163 @@
local M = {}
function M.fes_version()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.fes and fes_mod.config.fes.version then
return fes_mod.config.fes.version
end
return ""
end
function M.element(tag, attrs, content)
local out = { "<", tag }
function M.site_version()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.version then
return fes_mod.config.site.version
if attrs then
for k, v in pairs(attrs) do
if v ~= false and v ~= nil then
if v == true then
out[#out + 1] = " " .. k
else
out[#out + 1] = " " .. k .. "=\"" .. tostring(v) .. "\""
end
end
end
end
return ""
if content == nil then
out[#out + 1] = " />"
return table.concat(out)
end
out[#out + 1] = ">"
out[#out + 1] = tostring(content)
out[#out + 1] = "</"
out[#out + 1] = tag
out[#out + 1] = ">"
return table.concat(out)
end
function M.a(link, str)
link = link or "https://example.com"
str = str or link
return "<a href=\"" .. link .. "\">" .. str .. "</a>"
return M.element("a", { href = link }, str)
end
function M.download(link, str, downloadName)
link = link or "."
str = str or link
return M.element("a", { href = link, download = downloadName }, str)
end
function M.ha(link, str)
link = link or "https://example.com"
str = str or link
return "<a class=\"hidden\" href=\"" .. link .. "\">" .. str .. "</a>"
return M.element("a", { href = link, class = "hidden" }, str)
end
function M.external(link, str)
return "<a target=\"_blank\" href=\"" .. link .. "\">" .. str .. "</a>"
return M.element("a", { href = link, target = "_blank" }, str)
end
function M.note(str)
return '<div class="note">' .. str .. '</div>'
return M.element("div", { class = "note" }, str)
end
function M.muted(str)
return '<div class="muted">' .. str .. '</div>'
return M.element("div", { class = "muted" }, str)
end
function M.callout(str)
return '<div class="callout">' .. str .. '</div>'
return M.element("div", { class = "callout" }, str)
end
function M.h1(str)
return "<h1>" .. (str or "") .. "</h1>"
return M.element("h1", nil, str or "")
end
function M.h2(str)
return "<h2>" .. (str or "") .. "</h2>"
return M.element("h2", nil, str or "")
end
function M.h3(str)
return "<h3>" .. (str or "") .. "</h3>"
return M.element("h3", nil, str or "")
end
function M.h4(str) return "<h4>" .. (str or "") .. "</h4>"
function M.h4(str)
return M.element("h4", nil, str or "")
end
function M.h5(str)
return "<h5>" .. (str or "") .. "</h5>"
return M.element("h5", nil, str or "")
end
function M.h6(str)
return "<h6>" .. (str or "") .. "</h6>"
return M.element("h6", nil, str or "")
end
function M.p(str)
return "<p>" .. (str or "") .. "</p>"
return M.element("p", nil, str or "")
end
function M.pre(str)
return "<pre>" .. (str or "") .. "</pre>"
return M.element("pre", nil, str or "")
end
function M.code(str)
return "<pre><code>" .. (str or "") .. "</code></pre>"
return M.element("pre", nil, M.element("code", nil, str or ""))
end
function M.ul(items)
items = items or {}
local html = "<ul>"
local out = {}
for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>"
out[#out + 1] = M.element("li", nil, item)
end
html = html .. "</ul>"
return html
return M.element("ul", nil, table.concat(out))
end
function M.ol(items)
items = items or {}
local html = "<ol>"
local out = {}
for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>"
out[#out + 1] = M.element("li", nil, item)
end
html = html .. "</ol>"
return html
return M.element("ol", nil, table.concat(out))
end
function M.tl(items)
items = items or {}
local html = '<ul class="tl">'
local out = {}
for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>"
out[#out + 1] = M.element("li", nil, item)
end
html = html .. "</ul>"
return html
return M.element("ul", { class = "tl" }, table.concat(out))
end
function M.blockquote(str)
return "<blockquote>" .. (str or "") .. "</blockquote>"
return M.element("blockquote", nil, str or "")
end
function M.hr()
return "<hr>"
return M.element("hr")
end
function M.img(src, alt)
src = src or ""
alt = alt or ""
return '<img src="' .. src .. '" alt="' .. alt .. '">'
return M.element("img", { src = src or "", alt = alt or "" })
end
function M.strong(str)
return "<strong>" .. (str or "") .. "</strong>"
return M.element("strong", nil, str or "")
end
function M.em(str)
return "<em>" .. (str or "") .. "</em>"
return M.element("em", nil, str or "")
end
function M.br()
return "<br>"
return M.element("br")
end
function M.div(content, class)
content = content or ""
class = class or ""
local class_attr = class ~= "" and (' class="' .. class .. '"') or ""
return "<div" .. class_attr .. ">" .. content .. "</div>"
return M.element("div", class and { class = class } or nil, content or "")
end
function M.span(content, class)
content = content or ""
class = class or ""
local class_attr = class ~= "" and (' class="' .. class .. '"') or ""
return "<span" .. class_attr .. ">" .. content .. "</span>"
return M.element("span", class and { class = class } or nil, content or "")
end
-- HTML escaping utility
function M.escape(str)
str = tostring(str or "")
str = str:gsub("&", "&amp;")
@@ -159,55 +168,28 @@ function M.escape(str)
return str
end
-- Get site name from config
function M.site_name()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.name then
return fes_mod.config.site.name
end
return ""
end
-- Get site title from config
function M.site_title()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.title then
return fes_mod.config.site.title
end
return ""
end
-- Get site authors from config
function M.site_authors()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.authors then
return fes_mod.config.site.authors
end
return {}
end
function M.highlight(str)
return '<span class="highlight">' .. (str or "") .. "</span>"
return M.element("span", { class = "highlight" }, str or "")
end
function M.banner(str)
return '<div class="banner">' .. (str or "") .. "</div>"
return M.element("div", { class = "banner" }, str or "")
end
function M.center(str)
return '<div class="center">' .. (str or "") .. "</div>"
return M.element("div", { class = "center" }, str or "")
end
function M.nav(link, str)
link = link or "example.com"
str = str or link
return '<a class="nav" href="' .. link .. '">' .. str .. "</a>"
return M.element("a", { href = link, class = "nav" }, str)
end
function M.rl(r, l)
r = r or ""
l = l or ""
return string.format('<span class="left">%s</span><span class="right">%s</span>', r, l)
return
M.element("span", { class = "left" }, r or "") ..
M.element("span", { class = "right" }, l or "")
end
return M

View File

@@ -1,7 +1,75 @@
local M = {}
M.copyright = "&#169;"
M.registered_trademark = "&#174;"
M.trademark = "&#8482;"
local function get(s)
return "&" .. (s or "") .. ";"
end
M.legal = {
copyright = get("copy"),
registered_trademark = get("reg"),
trademark = get("trade"),
}
M.currency = {
euro = get("euro"),
pound = get("pound"),
yen = get("yen"),
cent = get("cent"),
dollar = "$",
}
M.math = {
plus_minus = get("plusmn"),
multiply = get("times"),
divide = get("divide"),
not_equal = get("ne"),
less_equal = get("le"),
greater_equal = get("ge"),
infinity = get("infin"),
approx = get("asymp"),
}
M.arrows = {
left = get("larr"),
right = get("rarr"),
up = get("uarr"),
down = get("darr"),
left_right = get("harr"),
}
M.punctuation = {
left_double_quote = get("ldquo"),
right_double_quote = get("rdquo"),
left_single_quote = get("lsquo"),
right_single_quote = get("rsquo"),
ellipsis = get("hellip"),
em_dash = get("mdash"),
en_dash = get("ndash"),
}
M.whitespace = {
non_breaking = get("nbsp"),
thin = get("thinsp"),
}
M.symbols = {
degree = get("deg"),
micro = get("micro"),
section = get("sect"),
paragraph = get("para"),
check = get("check"),
cross = get("cross"),
bullet = get("bull"),
middle_dot = get("middot"),
broken_bar = get("brvbar"),
}
M.html = {
less_than = get("lt"),
greater_than = get("gt"),
ampersand = get("amp"),
double_quote = get("quot"),
single_quote = get("apos"),
}
return M

View File

@@ -3,12 +3,33 @@ local symbol = require("lib.symbol")
local M = {}
function M.cc(tbl)
return table.concat(tbl)
function M.cc(tbl, sep)
return table.concat(tbl, sep or "")
end
function M.year(y)
return y or os.date("%Y")
end
function M.copyright(link, holder)
return symbol.copyright .. " " .. std.external(link, holder)
return symbol.legal.copyright .. " " .. std.external(link, holder)
end
function M.license(name)
return symbol.legal.registered .. " " .. name
end
function M.ls(dir)
local p = io.popen('ls -A -1 -- ' .. string.format('%q', dir))
if not p then
return nil
end
local t = {}
for line in p:lines() do
t[#t + 1] = line
end
p:close()
return t
end
return M

39
main.go
View File

@@ -2,10 +2,10 @@ package main
import (
"embed"
"errors"
"flag"
"fmt"
"os"
"runtime"
"github.com/fatih/color"
@@ -13,6 +13,7 @@ import (
"fes/modules/doc"
"fes/modules/new"
"fes/modules/server"
"fes/modules/ui"
"fes/modules/version"
)
@@ -25,21 +26,25 @@ var documentation string
func init() {
config.Port = flag.Int("p", 3000, "Set the server port")
config.Color = flag.Bool("no-color", false, "Disable color output")
config.Static = flag.Bool("static", false, "Render and save all pages.")
config.Static = flag.Bool("static", false, "Render and save all pages")
config.Docker = flag.Bool("docker", false, "Create a docker project")
config.Lib = lib
config.Doc = documentation
config.Verbose = flag.Bool("verbose", false, "Enable verbose logging")
}
func main() {
var m runtime.MemStats
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0])
fmt.Println("Commands:")
fmt.Println(" new <project_dir> Create a new project")
fmt.Println(" doc Open documentation")
fmt.Println(" run <project_dir> Start the server")
fmt.Println("Options:")
fmt.Fprintln(flag.CommandLine.Output(), "Commands:")
fmt.Fprintln(flag.CommandLine.Output(), " new <project_dir> Create a new project")
fmt.Fprintln(flag.CommandLine.Output(), " doc Open documentation")
fmt.Fprintln(flag.CommandLine.Output(), " run <project_dir> Start the server")
fmt.Fprintln(flag.CommandLine.Output(), "Options:")
flag.PrintDefaults()
fmt.Println("For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
fmt.Fprintln(flag.CommandLine.Output(), "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
}
showVersion := flag.Bool("version", false, "Show version and exit")
@@ -88,16 +93,16 @@ func main() {
os.Exit(1)
}
case "run":
if err := server.Start(dir); err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "%s does not exist\n", dir)
fmt.Fprintf(os.Stderr, "Try: fes new %s\n", dir)
os.Exit(1)
} else {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
if *config.Port == 3000 {
ui.WARNING("Using default port, this may lead to conflicts with other services")
}
ui.Log("Fes is starting")
ui.Log("Fes version=%s, commit=%s, just started", version.VERSION, version.GetCommit())
runtime.ReadMemStats(&m)
ui.Log("FRE memory usage when created %v Mb", m.TotalAlloc/1024/1024)
server.Start(dir)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
flag.Usage()

View File

@@ -10,6 +10,8 @@ var Doc string
var Port *int
var Color *bool
var Static *bool
var Docker *bool
var Verbose *bool
type AppConfig struct {
App struct {

View File

@@ -9,6 +9,7 @@ import (
"github.com/pkg/browser"
)
/* open documentation in browser */
func Open() error {
fmt.Println("Opening documentation in browser")

View File

@@ -1,6 +1,8 @@
package new
import (
"fes/modules/config"
"fes/modules/ui"
"fmt"
"os"
"os/exec"
@@ -26,7 +28,7 @@ func getName() string {
}
/* helper function for writing files */
func write(path string, format string, args ...interface{}) error {
func write(path string, format string, args ...any) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
panic(err)
@@ -49,6 +51,22 @@ func Project(dir string) error {
return err
}
if *config.Docker {
write("docker-compose.yml", `services:
%s:
image: git.vxserver.dev/fsd/fes:latest
ports:
- "3000:3000"
volumes:
- ./app:/app`, dir)
if err := os.Mkdir("app", 0755); err != nil {
return err
}
if err := os.Chdir("app"); err != nil {
return err
}
}
name := getName()
write("www/index.lua", `local fes = require("fes")
@@ -64,5 +82,49 @@ return site`, name)
name = "%s"
version = "0.0.1"
authors = ["%s"]`, dir, name)
write("README.md", strings.ReplaceAll(`# %s
$$$$$$
fes new %s
$$$$$$
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
$$$$$$
.
├── Fes.toml
├── README.md
└── www
└── index.lua
$$$$$$
Fes looks for $$.lua$$ files in the $$www/$$ directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| $$fes run .$$ | Runs the project at $$.$$ |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"), dir, dir)
ui.Hint("you can run this with `fes run %s`", dir)
fmt.Println("Created new Fes project at", func () string {
if cwd, err := os.Getwd(); err != nil {
return dir
} else {
return cwd
}
}())
return nil
}

6
modules/server/render.go Normal file
View File

@@ -0,0 +1,6 @@
package server
/* returns a string of rendered html */
func render(luapath string) (string, error) {
return "", nil
}

View File

@@ -2,441 +2,17 @@ package server
import (
"fes/modules/config"
"fes/modules/ui"
"fmt"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
)
type reqData struct {
path string
params map[string]string
}
func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error {
for _, entry := range entries {
path := filepath.Join(dir, entry.Name())
if entry.IsDir() {
nextBase := joinBase(base, entry.Name())
subEntries, err := os.ReadDir(path)
if err != nil {
return fmt.Errorf("failed to read directory %s: %w", path, err)
}
if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil {
return err
}
continue
}
route := joinBase(base, entry.Name())
if !isStatic && strings.HasSuffix(entry.Name(), ".lua") {
name := strings.TrimSuffix(entry.Name(), ".lua")
if name == "index" {
routes[basePath(base)] = path
routes[route] = path
continue
}
route = joinBase(base, name)
} else if !isStatic && strings.HasSuffix(entry.Name(), ".md") {
name := strings.TrimSuffix(entry.Name(), ".md")
if name == "index" {
routes[basePath(base)] = path
routes[route] = path
continue
}
route = joinBase(base, name)
}
routes[route] = path
}
return nil
}
func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable {
app := L.NewTable()
ents, err := os.ReadDir(includeDir)
if err != nil {
return app
}
for _, e := range ents {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") {
continue
}
base := strings.TrimSuffix(e.Name(), ".lua")
path := filepath.Join(includeDir, e.Name())
if _, err := os.Stat(path); err != nil {
tbl := L.NewTable()
tbl.RawSetString("error", lua.LString(fmt.Sprintf("file not found: %s", path)))
app.RawSetString(base, tbl)
continue
}
if err := L.DoFile(path); err != nil {
tbl := L.NewTable()
tbl.RawSetString("error", lua.LString(err.Error()))
app.RawSetString(base, tbl)
continue
}
val := L.Get(-1)
L.Pop(1)
tbl, ok := val.(*lua.LTable)
if !ok || tbl == nil {
tbl = L.NewTable()
}
app.RawSetString(base, tbl)
}
return app
}
func loadLua(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) {
L := lua.NewState()
defer L.Close()
libFiles, err := fs.ReadDir(config.Lib, "lib")
if err == nil {
for _, de := range libFiles {
if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") {
continue
}
path := filepath.Join("lib", de.Name())
fileData, err := config.Lib.ReadFile(path)
if err != nil {
continue
}
L.DoString(string(fileData))
}
}
preloadLuaModule := func(name, path string) {
L.PreloadModule(name, func(L *lua.LState) int {
fileData, err := config.Lib.ReadFile(path)
if err != nil {
panic(err)
}
if err := L.DoString(string(fileData)); err != nil {
panic(err)
}
L.Push(L.Get(-1))
return 1
})
}
preloadLuaModule("lib.std", "lib/std.lua")
preloadLuaModule("lib.symbol", "lib/symbol.lua")
preloadLuaModule("lib.util", "lib/util.lua")
L.PreloadModule("fes", func(L *lua.LState) int {
mod := L.NewTable()
libModules := []string{}
if ents, err := fs.ReadDir(config.Lib, "lib"); err == nil {
for _, e := range ents {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") {
continue
}
libModules = append(libModules, strings.TrimSuffix(e.Name(), ".lua"))
}
}
for _, modName := range libModules {
path := filepath.Join("lib", modName+".lua")
fileData, err := config.Lib.ReadFile(path)
if err != nil {
continue
}
if err := L.DoString(string(fileData)); err != nil {
continue
}
val := L.Get(-1)
L.Pop(1)
tbl, ok := val.(*lua.LTable)
if !ok || tbl == nil {
tbl = L.NewTable()
}
if modName == "fes" {
tbl.ForEach(func(k, v lua.LValue) { mod.RawSet(k, v) })
} else {
mod.RawSetString(modName, tbl)
}
}
mod.RawSetString("app", loadIncludeModules(L, filepath.Join(".", "include")))
if cfg != nil {
site := L.NewTable()
site.RawSetString("version", lua.LString(cfg.App.Version))
site.RawSetString("name", lua.LString(cfg.App.Name))
authors := L.NewTable()
for i, a := range cfg.App.Authors {
authors.RawSetInt(i+1, lua.LString(a))
}
site.RawSetString("authors", authors)
mod.RawSetString("site", site)
}
bus := L.NewTable()
bus.RawSetString("url", lua.LString(requestData.path))
params := L.NewTable()
for k, v := range requestData.params {
params.RawSetString(k, lua.LString(v))
}
bus.RawSetString("params", params)
mod.RawSetString("bus", bus)
mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int {
L.Push(lua.LString(markdownToHTML(L.ToString(1))))
return 1
}))
L.Push(mod)
return 1
})
if err := L.DoFile(entry); err != nil {
return []byte(""), err
}
if L.GetTop() == 0 {
return []byte(""), nil
}
L.SetGlobal("__fes_result", L.Get(-1))
if err := L.DoString("return tostring(__fes_result)"); err != nil {
L.GetGlobal("__fes_result")
if s := L.ToString(-1); s != "" {
return []byte(s), nil
}
return []byte(""), nil
}
if s := L.ToString(-1); s != "" {
return []byte(s), nil
}
return []byte(""), nil
}
func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
info, err := os.Stat(fsPath)
if err != nil {
return "", err
}
if !info.IsDir() {
return "", fmt.Errorf("not a directory")
}
ents, err := os.ReadDir(fsPath)
if err != nil {
return "", err
}
type entryInfo struct {
name string
isDir bool
href string
size int64
mod time.Time
}
var list []entryInfo
for _, e := range ents {
n := e.Name()
full := filepath.Join(fsPath, n)
st, err := os.Stat(full)
if err != nil {
continue
}
isd := st.IsDir()
displayName := n
if isd {
displayName = n + "/"
}
href := path.Join(urlPath, n)
if isd && !strings.HasSuffix(href, "/") {
href = href + "/"
}
size := int64(-1)
if !isd {
size = st.Size()
}
list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()})
}
sort.Slice(list, func(i, j int) bool {
if list[i].isDir != list[j].isDir {
return list[i].isDir
}
return strings.ToLower(list[i].name) < strings.ToLower(list[j].name)
})
urlPath = basePath(strings.TrimPrefix(urlPath, "/archive"))
var b strings.Builder
b.WriteString("<html>\n<head><title>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</title></head>\n<body>\n<h1>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</h1><hr><pre>")
if urlPath != "/archive" && urlPath != "/archive/" {
up := path.Dir(urlPath)
if up == "." {
up = "/archive"
}
if !strings.HasSuffix(up, "/") {
up = "/archive" + filepath.Dir(up) + "/"
}
b.WriteString(`<a href="` + template.HTMLEscapeString(up) + `">../</a>` + "\n")
} else {
b.WriteString(`<a href="../">../</a>` + "\n")
}
nameCol := 50
for _, ei := range list {
escapedName := template.HTMLEscapeString(ei.name)
dateStr := ei.mod.Local().Format("02-Jan-2006 15:04")
var sizeStr string
if ei.isDir {
sizeStr = "-"
} else {
sizeStr = fmt.Sprintf("%d", ei.size)
}
spaces := 1
if len(escapedName) < nameCol {
spaces = nameCol - len(escapedName)
}
line := `<a href="` + template.HTMLEscapeString(ei.href) + `">` + escapedName + `</a>` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n"
b.WriteString(line)
}
b.WriteString("</pre><hr></body>\n</html>")
return b.String(), nil
}
func generateNotFoundData(cfg *config.AppConfig) []byte {
notFoundData := []byte(`
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>fes</center>
</body>
</html>
`)
if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil {
if nf, err := loadLua("www/404.lua", cfg, reqData{}); err == nil {
notFoundData = nf
}
} else if _, err := os.Stat("www/404.html"); err == nil {
if buf, err := os.ReadFile("www/404.html"); err == nil {
notFoundData = buf
}
}
return notFoundData
}
func loadDirs() map[string]string {
routes := make(map[string]string)
if entries, err := os.ReadDir("www"); err == nil {
if err := handleDir(entries, "www", routes, "", false); err != nil {
ui.Warning("failed to handle www directory", err)
}
}
if entries, err := os.ReadDir("static"); err == nil {
if err := handleDir(entries, "static", routes, "/static", true); err != nil {
ui.Warning("failed to handle static directory", err)
}
}
if entries, err := os.ReadDir("archive"); err == nil {
if err := handleDir(entries, "archive", routes, "/archive", true); err != nil {
ui.Warning("failed to handle archive directory", err)
}
}
return routes
}
func parseConfig() config.AppConfig {
tomlDocument, err := os.ReadFile("Fes.toml")
if err != nil {
ui.Error("failed to read Fes.toml", err)
os.Exit(1)
}
docStr := fixMalformedToml(string(tomlDocument))
var cfg config.AppConfig
if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil {
ui.Warning("failed to parse Fes.toml", err)
cfg.App.Authors = []string{"unknown"}
cfg.App.Name = "unknown"
cfg.App.Version = "unknown"
}
return cfg
}
func readArchive(w http.ResponseWriter, route string) {
fsPath := "." + route
if info, err := os.Stat(fsPath); err == nil && info.IsDir() {
if page, err := generateArchiveIndex(fsPath, route); err == nil {
w.Write([]byte(page))
}
}
}
func Start(dir string) error {
if err := os.Chdir(dir); err != nil {
return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err)
}
cfg := parseConfig()
notFoundData := generateNotFoundData(&cfg)
routes := loadDirs()
var routes map[string]string
func Start(dir string) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
route, ok := routes[r.URL.Path]
var err error = nil
/* defer won't update paramaters unless we do this. */
defer func() {
ui.Path(route, err)
}()
if !ok {
err = config.ErrRouteMiss
route = r.URL.Path
if strings.HasPrefix(route, "/archive") {
readArchive(w, route)
} else {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(notFoundData))
}
return
}
params := make(map[string]string)
for k, v := range r.URL.Query() {
if len(v) > 0 {
params[k] = v[0]
}
}
var data []byte
if strings.HasSuffix(route, ".lua") {
data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params})
} else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data)))
} else {
data, err = os.ReadFile(route)
}
if err != nil {
http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError)
}
w.Write(data)
w.Write([]byte("<h1>Sup bitch</h1>"))
})
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)
return http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil))
}

View File

@@ -4,36 +4,8 @@ import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"regexp"
"strings"
)
func joinBase(base, name string) string {
if base == "" {
return "/" + name
}
return base + "/" + name
}
func basePath(base string) string {
if base == "" || base == "." {
return "/"
}
return base
}
func fixMalformedToml(content string) string {
re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`)
return re.ReplaceAllStringFunc(content, func(match string) string {
parts := strings.Split(strings.TrimSpace(match), "=")
if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" {
key := strings.TrimSpace(parts[0])
return key + " = \"\""
}
return match
})
}
func markdownToHTML(mdText string) string {
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)

View File

@@ -4,54 +4,114 @@ import (
"errors"
"fmt"
"strings"
"time"
"fes/modules/config"
"fes/modules/version"
"github.com/fatih/color"
)
const (
hintColor = 0xbda02a
)
func formatTimestamp() string {
return time.Now().Format("02 Jan 2006 15:04")
}
func logMessage(prefix string, msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
if prefix == "" {
fmt.Printf("%s * %s\n", formatTimestamp(), formatted)
} else {
fmt.Printf("%s * %s: %s\n", formatTimestamp(), prefix, formatted)
}
}
// Generic log
func Log(msg string, args ...any) {
logMessage("", msg, args...)
}
// OK message (green)
func OK(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Green("%s * %s\n", formatTimestamp(), formatted)
}
// Warning message (magenta)
func WARN(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Magenta("%s # %s\n", formatTimestamp(), formatted)
}
// Warning message (magenta)
func WARNING(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Magenta("%s # WARNING %s\n", formatTimestamp(), formatted)
}
// Error message (red)
func ERROR(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s ! %s\n", formatTimestamp(), formatted)
}
// Fatal message and panic
func FATAL(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s % %s\n", formatTimestamp(), formatted)
panic(formatted)
}
// Hint message (custom color)
func Hint(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.RGB(func(hex int) (r, g, b int) {
r = (hex >> 16) & 0xFF
g = (hex >> 8) & 0xFF
b = hex & 0xFF
return
}(hintColor)).Printf("hint: %s\n", formatted)
}
// Path logging: prints route and status
func Path(path string, err error) {
path = strings.TrimPrefix(path, "/")
if path == "" {
path = "(null)"
}
fmt.Printf(" > %s ", path)
if err == nil {
OK("ok")
return
OK("Route: %s - ok", path)
} else if errors.Is(err, config.ErrRouteMiss) {
WARN(config.ErrRouteMiss.Error())
WARN("Route: %s - %s", path, config.ErrRouteMiss.Error())
} else {
ERROR("bad")
ERROR("Route: %s - bad", path)
}
}
// System warning with prefix
func Warning(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err)
WARN("%s: %v", msg, err)
return err
}
// System error with prefix
func Error(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err)
ERROR("%s: %v", msg, err)
return err
}
// Fatal system error
func Fatal(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err)
panic(err)
FATAL("%s: %v", msg, err)
return err
}
func OK(msg string) {
color.Green(msg)
}
func WARN(msg string) {
color.Magenta(msg)
}
func ERROR(msg string) {
color.Red(msg)
// Log on Verbose
func LogVerbose(msg string, args ...any) {
if *config.Verbose {
Log(msg, args...)
}
}

View File

@@ -9,7 +9,7 @@ var gitCommit string = "devel"
const PROGRAM_NAME string = "fes"
const PROGRAM_NAME_LONG string = "fes/fSD"
const VERSION string = "beta"
const VERSION string = "0.3.1"
func Version() {
fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION)
@@ -20,3 +20,7 @@ func FullVersion() {
fmt.Printf("%s+%s\n", VERSION, gitCommit)
os.Exit(0)
}
func GetCommit() string {
return gitCommit
}