fes

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit a67a654491454dc0b78926633ab59ea321669115
parent c7b0fa6248b228a9ee83a83333ed3bdb503eb564
Author: vx-clutch <[email protected]>
Date:   Sun, 30 Nov 2025 18:48:30 -0500

alpha p4

Diffstat:
MTODO | 1-
Mcore/builtin.lua | 484+++++++++++++++++--------------------------------------------------------------
Mcore/std.lua | 6++++++
Mexamples/multi-page/www/index.lua | 1+
Aexamples/multi-page/www/sub/subpage.lua | 14++++++++++++++
Msrc/server/server.go | 54++++++++++++++++++++++++++++++++++++++++--------------
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 "&#169; 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 "&#169; 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 "&#169; 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 "&#169; 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) {