commit a67a654491454dc0b78926633ab59ea321669115
parent c7b0fa6248b228a9ee83a83333ed3bdb503eb564
Author: vx-clutch <[email protected]>
Date: Sun, 30 Nov 2025 18:48:30 -0500
alpha p4
Diffstat:
6 files changed, 164 insertions(+), 396 deletions(-)
diff --git a/TODO b/TODO
@@ -1,3 +1,2 @@
Add an interval element
Add a way to pass data to sites on load via fes.bus
-Fire emoji default favicon
diff --git a/core/builtin.lua b/core/builtin.lua
@@ -3,24 +3,29 @@ local std = require("core.std")
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 function encode(str)
+ return str:gsub("([^%w%-%_%.%~])", function(c)
+ return string.format("%%%02X", string.byte(c)) end) end
- local self = {
- version = site_config.version or "",
- title = site_config.title or "Document",
- copyright = site_config.copyright or "© The Copyright Holder",
- favicon = "/image/favicon.ico",
- header = header or [[
+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 raw_favicon = site_config.favicon or [[<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔥</text></svg>]]
+
+ local self = {
+ version = site_config.version or "",
+ title = site_config.title or "Document",
+ copyright = site_config.copyright or "© The Copyright Holder",
+ favicon = "data:image/svg+xml," .. encode(raw_favicon),
+ header = header or [[
<!DOCTYPE html>
<html lang="en">
<head>
@@ -29,357 +34,71 @@ function M.fes(header, footer)
<link rel="icon" href="{{FAVICON}}">
<title>{{TITLE}}</title>
<style>
-html, body {
- min-height: 100%;
- margin: 0;
- padding: 0;
- background: #0f1113;
- color: #e6eef3;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- line-height: 1.5;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- padding: 36px;
-}
-
-.container {
- max-width: 830px;
- margin: 0 auto;
-}
-
-.container > *:not(.banner) {
- margin: 28px 0;
-}
-
-h1, h2, h3, h4, h5, h6 {
- font-weight: 600;
- margin: 0 0 12px 0;
-}
-
-h1 {
- font-size: 40px;
- margin-bottom: 20px;
- font-weight: 700;
-}
-
-h2 {
- font-size: 32px;
- margin: 26px 0 14px;
-}
-
-h3 {
- font-size: 26px;
- margin: 22px 0 12px;
-}
-
-h4 {
- font-size: 20px;
- margin: 18px 0 10px;
-}
-
-h5 {
- font-size: 16px;
- margin: 16px 0 8px;
-}
-
-h6 {
- font-size: 14px;
- margin: 14px 0 6px;
- color: #9aa6b1;
-}
-
-p {
- margin: 14px 0;
-}
-
-a {
- color: #68a6ff;
- text-decoration: none;
- transition: color .15s ease, text-decoration-color .15s ease;
-}
-
-.hidden {
- color: #dfe9ee;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-summary {
- cursor: pointer;
-}
-
-details {
- background: #1a1c20;
- border: 1px solid rgba(255,255,255,.06);
- border-radius: 4px;
- padding: 14px 16px;
- margin: 16px 0;
-}
-
-details summary {
- list-style: none;
- font-weight: 600;
- color: #e6eef3;
- display: flex;
- align-items: center;
-}
-
-details summary::-webkit-details-marker {
- display: none;
-}
-
-details summary::before {
- content: "â–¸";
- margin-right: 8px;
- transition: transform .15s ease;
- color: #68a6ff;
-}
-
-details[open] summary::before {
- transform: rotate(90deg);
-}
-
-summary::after {
- content: "Expand";
- margin-left: auto;
- font-size: 13px;
- color: #9aa6b1;
-}
-
-details[open] summary::after {
- content: "Collapse";
-}
-
-details > *:not(summary) {
- margin-top: 12px;
-}
-
-.note, pre, code {
- background: #1a1c20;
- border: 1px solid rgba(255,255,255,.06);
-}
-
-.note {
- padding: 20px;
- border-radius: 4px;
- background: #1a1c20;
- border: 1px solid rgba(255,255,255,.06);
- margin: 28px 0;
- color: #dfe9ee;
-}
-
-.note strong {
- color: #f0f6f8;
-}
-
-.muted {
- color: #9aa6b1;
-}
-
-.lead {
- font-size: 15px;
- margin-top: 8px;
-}
-
-.callout {
- display: block;
- margin: 12px 0;
-}
-
-.small {
- font-size: 13px;
- color: #9aa6b1;
- margin-top: 6px;
-}
-
-.highlight {
- font-weight: 700;
- color: #cde7ff;
-}
-
-ul, ol {
- margin: 14px 0;
- padding-left: 26px;
-}
-
-.tl {
- display: grid;
- grid-template-columns: repeat(auto-fill, 200px);
- gap: 15px;
- list-style-type: none;
- padding: 0;
- margin: 0;
- justify-content: start;
-}
-
-ul.tl li {
- padding: 10px;
- width: fit-content;
-}
-
-li {
- margin: 6px 0;
-}
-
-code {
- padding: 3px 7px;
- border-radius: 3px;
- font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
- font-size: .9em;
- color: #cde7ff;
-}
-
-pre {
- padding: 20px;
- border-radius: 4px;
- margin: 14px 0;
- overflow-x: auto;
- font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
- font-size: 14px;
- line-height: 1.6;
-}
-
-pre code {
- background: none;
- border: none;
- padding: 0;
- font-size: inherit;
-}
-
-blockquote {
- border-left: 3px solid #68a6ff;
- padding-left: 18px;
- margin: 14px 0;
- color: #dfe9ee;
- font-style: italic;
-}
-
-hr {
- border: 0;
- border-top: 1px solid rgba(255,255,255,.1);
- margin: 26px 0;
-}
-
-img {
- max-width: 100%;
- height: auto;
- border-radius: 4px;
- margin: 14px 0;
-}
-
-table {
- width: 100%;
- border-collapse: collapse;
- margin: 14px 0;
-}
-
-th, td {
- padding: 12px 16px;
- text-align: left;
- border-bottom: 1px solid rgba(255,255,255,.06);
-}
-
-th {
- background: #1a1c20;
- font-weight: 600;
- color: #f0f6f8;
-}
-
-tr:hover {
- background: rgba(255,255,255,.02);
-}
-
-.divider {
- margin: 26px 0;
- height: 1px;
- background: rgba(255,255,255,.1);
-}
-
-.section {
- margin-top: 36px;
-}
-
-.links {
- margin: 12px 0;
-}
-
-.links a {
- display: inline-block;
- margin: 0 14px 6px 0;
-}
-
-strong, b {
- font-weight: 600;
- color: #f0f6f8;
-}
-
-em, i {
- font-style: italic;
-}
-
-.note {
- padding: 20px;
- border-radius: 4px;
- background: #1a1c20;
- border: 1px solid rgba(255,255,255,.06);
- margin: 28px 0;
- color: #dfe9ee;
-}
-
-.center {
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.banner {
- width: 100%;
- box-sizing: border-box;
- text-align: center;
- background: #1a1c20;
- padding: 20px;
- border: 1px solid rgba(255,255,255,.06);
- border-bottom-right-radius: 8px;
- border-bottom-left-radius: 8px;
- color: #e6eef3;
- margin: -36px 0 28px 0;
- box-shadow: 0 0.2em 0.6em rgba(0,0,0,.4);
-}
-
-.nav {
- margin-left: auto;
- margin-right: auto;
-}
-
-.nav a {
- color: #cde7ff;
-}
-
-.footer {
- background: #1a1c20;
- padding: 20px 0;
- border-top: 1px solid rgba(255,255,255,.1);
- font-size: 14px;
- color: #d4dde3;
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 24px;
- margin-top: 28px !important;
- margin-bottom: 0;
-}
+html, body { min-height: 100%; margin: 0; padding: 0; background: #0f1113; color: #e6eef3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
+body { padding: 36px; }
+.container { max-width: 830px; margin: 0 auto; }
+.container > *:not(.banner) { margin: 28px 0; }
+h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; }
+h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; }
+h2 { font-size: 32px; margin: 26px 0 14px; }
+h3 { font-size: 26px; margin: 22px 0 12px; }
+h4 { font-size: 20px; margin: 18px 0 10px; }
+h5 { font-size: 16px; margin: 16px 0 8px; }
+h6 { font-size: 14px; margin: 14px 0 6px; color: #9aa6b1; }
+p { margin: 14px 0; }
+a { color: #68a6ff; text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; }
+.hidden { color: #dfe9ee; text-decoration: none; }
+a:hover { text-decoration: underline; }
+summary { cursor: pointer; }
+details { background: #1a1c20; border: 1px solid rgba(255,255,255,.06); border-radius: 4px; padding: 14px 16px; margin: 16px 0; }
+details summary { list-style: none; font-weight: 600; color: #e6eef3; display: flex; align-items: center; }
+details summary::-webkit-details-marker { display: none; }
+details summary::before { content: "â–¸"; margin-right: 8px; transition: transform .15s ease; color: #68a6ff; }
+details[open] summary::before { transform: rotate(90deg); }
+summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: #9aa6b1; }
+details[open] summary::after { content: "Collapse"; }
+details > *:not(summary) { margin-top: 12px; }
+.note, pre, code { background: #1a1c20; border: 1px solid rgba(255,255,255,.06); }
+.note { padding: 20px; border-radius: 4px; background: #1a1c20; border: 1px solid rgba(255,255,255,.06); margin: 28px 0; color: #dfe9ee; }
+.note strong { color: #f0f6f8; }
+.muted { color: #9aa6b1; }
+.lead { font-size: 15px; margin-top: 8px; }
+.callout { display: block; margin: 12px 0; }
+.small { font-size: 13px; color: #9aa6b1; margin-top: 6px; }
+.highlight { font-weight: 700; color: #cde7ff; }
+ul, ol { margin: 14px 0; padding-left: 26px; }
+.tl { display: grid; grid-template-columns: repeat(auto-fill, 200px); gap: 15px; list-style-type: none; padding: 0; margin: 0; justify-content: start; }
+ul.tl li { padding: 10px; width: fit-content; }
+li { margin: 6px 0; }
+code { padding: 3px 7px; border-radius: 3px; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: .9em; color: #cde7ff; }
+pre { padding: 20px; border-radius: 4px; margin: 14px 0; overflow-x: auto; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: 14px; line-height: 1.6; }
+pre code { background: none; border: none; padding: 0; font-size: inherit; }
+blockquote { border-left: 3px solid #68a6ff; padding-left: 18px; margin: 14px 0; color: #dfe9ee; font-style: italic; }
+hr { border: 0; border-top: 1px solid rgba(255,255,255,.1); margin: 26px 0; }
+img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; }
+table { width: 100%; border-collapse: collapse; margin: 14px 0; }
+th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid rgba(255,255,255,.06); }
+th { background: #1a1c20; font-weight: 600; color: #f0f6f8; }
+tr:hover { background: rgba(255,255,255,.02); }
+.divider { margin: 26px 0; height: 1px; background: rgba(255,255,255,.1); }
+.section { margin-top: 36px; }
+.links { margin: 12px 0; }
+.links a { display: inline-block; margin: 0 14px 6px 0; }
+strong, b { font-weight: 600; color: #f0f6f8; }
+em, i { font-style: italic; }
+.center { display: flex; justify-content: center; align-items: center; }
+.banner { width: 100%; box-sizing: border-box; text-align: center; background: #1a1c20; padding: 20px; border: 1px solid rgba(255,255,255,.06); border-bottom-right-radius: 8px; border-bottom-left-radius: 8px; color: #e6eef3; margin: -36px 0 28px 0; box-shadow: 0 0.2em 0.6em rgba(0,0,0,.4); }
+.nav { margin-left: auto; margin-right: auto; }
+.nav a { color: #cde7ff; }
+.footer { background: #1a1c20; padding: 20px 0; border-top: 1px solid rgba(255,255,255,.1); font-size: 14px; color: #d4dde3; display: flex; justify-content: center; align-items: center; gap: 24px; margin-top: 28px !important; margin-bottom: 0; }
+.left { text-align: left; float: left; }
+.right { text-align: right; float: right; }
</style>
</head>
<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>
@@ -390,36 +109,39 @@ em, i {
</body>
</html>
]],
- parts = {}
- }
- return setmetatable(self, M)
+ parts = {}
+ }
+
+ return setmetatable(self, M)
end
function M:custom(str)
- table.insert(self.parts, str)
- return self
+ table.insert(self.parts, str)
+ 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")
- header = header:gsub("{{FAVICON}}", self.favicon or "")
- local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "© The Copyright Holder")
- return header .. table.concat(self.parts, "\n") .. footer
+ local header = self.header
+ local safe_title = self.title or "Document"
+ local safe_favicon = self.favicon:gsub("%%", "%%%%")
+ header = header:gsub("{{TITLE}}", safe_title)
+ header = header:gsub("{{FAVICON}}", safe_favicon)
+ local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "© 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
diff --git a/core/std.lua b/core/std.lua
@@ -244,4 +244,10 @@ function M.nav(link, str)
return '<a class="nav" href="' .. link .. '">' .. str .. "</a>"
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)
+end
+
return M
diff --git a/examples/multi-page/www/index.lua b/examples/multi-page/www/index.lua
@@ -9,6 +9,7 @@ site:note(
fes.std.ul({
fes.std.a("page1"),
fes.std.a("page2"),
+ fes.std.a("sub/subpage"),
})
)
diff --git a/examples/multi-page/www/sub/subpage.lua b/examples/multi-page/www/sub/subpage.lua
@@ -0,0 +1,14 @@
+local fes = require("fes")
+local site = fes.fes()
+
+site.title = "Subpage"
+site.copyright = fes.util.copyright("https://git.vxserver.dev/fSD", "fSD")
+
+site:h1("Subpage")
+site:note(
+ fes.std.ul({
+ fes.std.a("/", "Home"),
+ })
+)
+
+return site
diff --git a/src/server/server.go b/src/server/server.go
@@ -18,6 +18,43 @@ import (
"fes/src/config"
)
+func handleDir(entries []os.DirEntry, wwwDir string, routes map[string]string, base string) error {
+ for _, entry := range entries {
+ if entry.IsDir() {
+ sub := filepath.Join(wwwDir, entry.Name())
+ subs, err := os.ReadDir(sub)
+ if err != nil {
+ return fmt.Errorf("failed to read %s: %w", sub, err)
+ }
+ next := base + "/" + entry.Name()
+ if err := handleDir(subs, sub, routes, next); err != nil {
+ return err
+ }
+ continue
+ }
+ if strings.HasSuffix(entry.Name(), ".lua") {
+ name := strings.TrimSuffix(entry.Name(), ".lua")
+ path := filepath.Join(wwwDir, entry.Name())
+ if name == "index" {
+ if base == "" {
+ routes["/"] = path
+ routes["/index"] = path
+ } else {
+ routes[base] = path
+ routes[base+"/index"] = path
+ }
+ } else {
+ if base == "" {
+ routes["/"+name] = path
+ } else {
+ routes[base+"/"+name] = path
+ }
+ }
+ }
+ }
+ return nil
+}
+
func fixMalformedToml(content string) string {
re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`)
return re.ReplaceAllStringFunc(content, func(match string) string {
@@ -209,11 +246,11 @@ func loadLua(luaDir string, entry string, cfg *config.MyConfig) (string, error)
}
func Start(dir string) error {
- doc, err := os.ReadFile(filepath.Join(dir, "Fes.toml"))
+ tomlDocument, err := os.ReadFile(filepath.Join(dir, "Fes.toml"))
if err != nil {
return err
}
- docStr := fixMalformedToml(string(doc))
+ docStr := fixMalformedToml(string(tomlDocument))
var cfg config.MyConfig
err = toml.Unmarshal([]byte(docStr), &cfg)
if err != nil {
@@ -227,18 +264,7 @@ func Start(dir string) error {
}
routes := make(map[string]string)
- for _, entry := range entries {
- if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lua") {
- baseName := strings.TrimSuffix(entry.Name(), ".lua")
- luaPath := filepath.Join(wwwDir, entry.Name())
- if baseName == "index" {
- routes["/"] = luaPath
- routes["/index"] = luaPath
- } else {
- routes["/"+baseName] = luaPath
- }
- }
- }
+ handleDir(entries, wwwDir, routes, "")
for route, luaPath := range routes {
func(rt string, lp string) {