This commit is contained in:
2025-11-30 18:48:30 -05:00
parent c7b0fa6248
commit a67a654491
6 changed files with 162 additions and 394 deletions

1
TODO
View File

@@ -1,3 +1,2 @@
Add an interval element Add an interval element
Add a way to pass data to sites on load via fes.bus Add a way to pass data to sites on load via fes.bus
Fire emoji default favicon

View File

@@ -3,24 +3,29 @@ local std = require("core.std")
local M = {} local M = {}
M.__index = M M.__index = M
local function encode(str)
return str:gsub("([^%w%-%_%.%~])", function(c)
return string.format("%%%02X", string.byte(c)) end) end
function M.fes(header, footer) function M.fes(header, footer)
local config = {} local config = {}
local site_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 fes_mod = package.loaded.fes 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>]]
if fes_mod and fes_mod.config then
config = fes_mod.config
if config.site then
site_config = config.site
end
end
local self = { local self = {
version = site_config.version or "", version = site_config.version or "",
title = site_config.title or "Document", title = site_config.title or "Document",
copyright = site_config.copyright or "&#169; The Copyright Holder", copyright = site_config.copyright or "&#169; The Copyright Holder",
favicon = "/image/favicon.ico", favicon = "data:image/svg+xml," .. encode(raw_favicon),
header = header or [[ header = header or [[
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -29,357 +34,71 @@ function M.fes(header, footer)
<link rel="icon" href="{{FAVICON}}"> <link rel="icon" href="{{FAVICON}}">
<title>{{TITLE}}</title> <title>{{TITLE}}</title>
<style> <style>
html, body { 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; }
min-height: 100%; body { padding: 36px; }
margin: 0; .container { max-width: 830px; margin: 0 auto; }
padding: 0; .container > *:not(.banner) { margin: 28px 0; }
background: #0f1113; h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; }
color: #e6eef3; h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; }
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; h2 { font-size: 32px; margin: 26px 0 14px; }
line-height: 1.5; h3 { font-size: 26px; margin: 22px 0 12px; }
-webkit-font-smoothing: antialiased; h4 { font-size: 20px; margin: 18px 0 10px; }
-moz-osx-font-smoothing: grayscale; h5 { font-size: 16px; margin: 16px 0 8px; }
} h6 { font-size: 14px; margin: 14px 0 6px; color: #9aa6b1; }
p { margin: 14px 0; }
body { a { color: #68a6ff; text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; }
padding: 36px; .hidden { color: #dfe9ee; text-decoration: none; }
} a:hover { text-decoration: underline; }
summary { cursor: pointer; }
.container { details { background: #1a1c20; border: 1px solid rgba(255,255,255,.06); border-radius: 4px; padding: 14px 16px; margin: 16px 0; }
max-width: 830px; details summary { list-style: none; font-weight: 600; color: #e6eef3; display: flex; align-items: center; }
margin: 0 auto; 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); }
.container > *:not(.banner) { summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: #9aa6b1; }
margin: 28px 0; 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); }
h1, h2, h3, h4, h5, h6 { .note { padding: 20px; border-radius: 4px; background: #1a1c20; border: 1px solid rgba(255,255,255,.06); margin: 28px 0; color: #dfe9ee; }
font-weight: 600; .note strong { color: #f0f6f8; }
margin: 0 0 12px 0; .muted { color: #9aa6b1; }
} .lead { font-size: 15px; margin-top: 8px; }
.callout { display: block; margin: 12px 0; }
h1 { .small { font-size: 13px; color: #9aa6b1; margin-top: 6px; }
font-size: 40px; .highlight { font-weight: 700; color: #cde7ff; }
margin-bottom: 20px; ul, ol { margin: 14px 0; padding-left: 26px; }
font-weight: 700; .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; }
h2 { 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; }
font-size: 32px; 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; }
margin: 26px 0 14px; 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; }
h3 { img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; }
font-size: 26px; table { width: 100%; border-collapse: collapse; margin: 14px 0; }
margin: 22px 0 12px; 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); }
h4 { .divider { margin: 26px 0; height: 1px; background: rgba(255,255,255,.1); }
font-size: 20px; .section { margin-top: 36px; }
margin: 18px 0 10px; .links { margin: 12px 0; }
} .links a { display: inline-block; margin: 0 14px 6px 0; }
strong, b { font-weight: 600; color: #f0f6f8; }
h5 { em, i { font-style: italic; }
font-size: 16px; .center { display: flex; justify-content: center; align-items: center; }
margin: 16px 0 8px; .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; }
h6 { .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; }
font-size: 14px; .left { text-align: left; float: left; }
margin: 14px 0 6px; .right { text-align: right; float: right; }
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;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
]], ]],
footer = footer or [[ footer = footer or [[
<footer class="footer"> <footer class="footer">
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a> <a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
<a href="https://www.lua.org/" target="_blank">Lua Powered</a> <a href="https://www.lua.org/" target="_blank">Lua Powered</a>
@@ -390,36 +109,39 @@ em, i {
</body> </body>
</html> </html>
]], ]],
parts = {} parts = {}
} }
return setmetatable(self, M)
return setmetatable(self, M)
end end
function M:custom(str) function M:custom(str)
table.insert(self.parts, str) table.insert(self.parts, str)
return self return self
end end
for name, func in pairs(std) do for name, func in pairs(std) do
if type(func) == "function" then if type(func) == "function" then
M[name] = function(self, ...) M[name] = function(self, ...)
local result = func(...) local result = func(...)
table.insert(self.parts, result) table.insert(self.parts, result)
return self return self
end end
end end
end end
function M:build() function M:build()
local header = self.header local header = self.header
header = header:gsub("{{TITLE}}", self.title or "Document") local safe_title = self.title or "Document"
header = header:gsub("{{FAVICON}}", self.favicon or "") local safe_favicon = self.favicon:gsub("%%", "%%%%")
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") header = header:gsub("{{TITLE}}", safe_title)
return header .. table.concat(self.parts, "\n") .. footer 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 end
M.__tostring = function(self) M.__tostring = function(self)
return self:build() return self:build()
end end
return M return M

View File

@@ -244,4 +244,10 @@ function M.nav(link, str)
return '<a class="nav" href="' .. link .. '">' .. str .. "</a>" return '<a class="nav" href="' .. link .. '">' .. str .. "</a>"
end 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 return M

View File

@@ -9,6 +9,7 @@ site:note(
fes.std.ul({ fes.std.ul({
fes.std.a("page1"), fes.std.a("page1"),
fes.std.a("page2"), fes.std.a("page2"),
fes.std.a("sub/subpage"),
}) })
) )

View File

@@ -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

View File

@@ -18,6 +18,43 @@ import (
"fes/src/config" "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 { func fixMalformedToml(content string) string {
re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`) re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`)
return re.ReplaceAllStringFunc(content, func(match string) string { 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 { 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 { if err != nil {
return err return err
} }
docStr := fixMalformedToml(string(doc)) docStr := fixMalformedToml(string(tomlDocument))
var cfg config.MyConfig var cfg config.MyConfig
err = toml.Unmarshal([]byte(docStr), &cfg) err = toml.Unmarshal([]byte(docStr), &cfg)
if err != nil { if err != nil {
@@ -227,18 +264,7 @@ func Start(dir string) error {
} }
routes := make(map[string]string) routes := make(map[string]string)
for _, entry := range entries { handleDir(entries, wwwDir, routes, "")
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
}
}
}
for route, luaPath := range routes { for route, luaPath := range routes {
func(rt string, lp string) { func(rt string, lp string) {