fes

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

commit ca435c01a8c8d22ebec50f6166809ee44726d8c5
parent 442e5c7f126be1ecfc1fd071871c5ad17ca3eb1b
Author: vx-clutch <[email protected]>
Date:   Thu, 20 Nov 2025 16:57:46 -0500

alpha

Diffstat:
Mcore/builtin.lua | 327++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcore/std.lua | 193++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Adoc/Fes.toml | 9+++++++++
Adoc/www/index.lua | 38++++++++++++++++++++++++++++++++++++++
Mmain.go | 55++++++++++++++++++++++++++++++++++++++-----------------
5 files changed, 601 insertions(+), 21 deletions(-)

diff --git a/core/builtin.lua b/core/builtin.lua @@ -15,9 +15,14 @@ function M.site_builder(header, footer) local self = { version = site_config.version or "", + title = site_config.title or "Document", header = header or [[ <!DOCTYPE html> <html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{{TITLE}}</title> <style> html, body { @@ -102,7 +107,149 @@ function M.site_builder(header, footer) .section { margin-top: 18px } + + h2 { + font-size: 32px; + margin: 24px 0 14px 0; + font-weight: 600; + } + + h3 { + font-size: 26px; + margin: 20px 0 12px 0; + font-weight: 600; + } + + h4 { + font-size: 20px; + margin: 16px 0 10px 0; + font-weight: 600; + } + + h5 { + font-size: 16px; + margin: 14px 0 8px 0; + font-weight: 600; + } + + h6 { + font-size: 14px; + margin: 12px 0 6px 0; + font-weight: 600; + color: #9aa6b1; + } + + ul, ol { + margin: 12px 0; + padding-left: 24px; + } + + li { + margin: 6px 0; + } + + code { + background: #17191b; + border: 1px solid rgba(255, 255, 255, 0.06); + padding: 2px 6px; + border-radius: 3px; + font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; + font-size: 0.9em; + color: #cde7ff; + } + + pre { + background: #17191b; + border: 1px solid rgba(255, 255, 255, 0.06); + padding: 18px; + border-radius: 4px; + margin: 12px 0; + overflow-x: auto; + font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; + font-size: 14px; + line-height: 1.5; + } + + pre code { + background: none; + border: none; + padding: 0; + font-size: inherit; + } + + blockquote { + border-left: 3px solid #68a6ff; + padding-left: 18px; + margin: 12px 0; + color: #dfe9ee; + font-style: italic; + } + + hr { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin: 24px 0; + } + + img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 12px 0; + } + + table { + width: 100%; + border-collapse: collapse; + margin: 12px 0; + } + + th, td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } + + th { + background: #17191b; + font-weight: 600; + color: #f0f6f8; + } + + tr:hover { + background: rgba(255, 255, 255, 0.02); + } + + .divider { + margin: 24px 0; + height: 1px; + background: rgba(255, 255, 255, 0.1); + } + + .section { + margin-top: 32px; + } + + .links { + margin: 10px 0; + } + + .links a { + display: inline-block; + margin-right: 12px; + margin-bottom: 6px; + } + + strong, b { + font-weight: 600; + color: #f0f6f8; + } + + em, i { + font-style: italic; + } </style> +</head> <body> <div class="container"> ]], @@ -174,18 +321,194 @@ function M:note(content) return self end +function M:muted(content) + content = content or "" + self:custom('<div class="muted quiet">' .. content .. '</div>') + return self +end + function M:a(link, str) - str = str or "" + link = link or "example.com" + str = str or link table.insert(self.parts, "<a href=\"" .. link .. "\">" .. str .. "</a>") return self end +function M:external(link, str) + link = link or "example.com" + str = str or link + table.insert(self.parts, "<a target=\"_blank\" href=\"" .. link .. "\">" .. str .. "</a>") + return self +end + function M:version() return self.version end +function M:ul(items) + items = items or {} + local html = "<ul>" + for _, item in ipairs(items) do + html = html .. "<li>" .. tostring(item) .. "</li>" + end + html = html .. "</ul>" + self:custom(html) + return self +end + +function M:ol(items) + items = items or {} + local html = "<ol>" + for _, item in ipairs(items) do + html = html .. "<li>" .. tostring(item) .. "</li>" + end + html = html .. "</ol>" + self:custom(html) + return self +end + +function M:li(str) + str = str or "" + self:custom("<li>" .. str .. "</li>") + return self +end + +function M:code(str) + str = str or "" + self:custom("<code>" .. str .. "</code>") + return self +end + +function M:pre(str) + str = str or "" + self:custom("<pre><code>" .. str .. "</code></pre>") + return self +end + +function M:blockquote(str) + str = str or "" + self:custom("<blockquote>" .. str .. "</blockquote>") + return self +end + +function M:hr() + self:custom("<hr>") + return self +end + +function M:divider() + self:custom('<div class="divider"></div>') + return self +end + +function M:img(src, alt) + src = src or "" + alt = alt or "" + self:custom('<img src="' .. src .. '" alt="' .. alt .. '">') + return self +end + +function M:table(headers, rows) + headers = headers or {} + rows = rows or {} + + local html = "<table><thead><tr>" + for _, header in ipairs(headers) do + html = html .. "<th>" .. tostring(header) .. "</th>" + end + html = html .. "</tr></thead><tbody>" + + for _, row in ipairs(rows) do + html = html .. "<tr>" + for _, cell in ipairs(row) do + html = html .. "<td>" .. tostring(cell) .. "</td>" + end + html = html .. "</tr>" + end + + html = html .. "</tbody></table>" + self:custom(html) + return self +end + +function M:div(content, class) + content = content or "" + class = class or "" + local class_attr = class ~= "" and (' class="' .. class .. '"') or "" + self:custom("<div" .. class_attr .. ">" .. content .. "</div>") + return self +end + +function M:span(content, class) + content = content or "" + class = class or "" + local class_attr = class ~= "" and (' class="' .. class .. '"') or "" + self:custom("<span" .. class_attr .. ">" .. content .. "</span>") + return self +end + +function M:strong(str) + str = str or "" + self:custom("<strong>" .. str .. "</strong>") + return self +end + +function M:em(str) + str = str or "" + self:custom("<em>" .. str .. "</em>") + return self +end + +function M:br() + self:custom("<br>") + return self +end + +function M:section(content) + content = content or "" + self:custom('<div class="section">' .. content .. '</div>') + return self +end + +function M:links(link_list) + link_list = link_list or {} + local html = '<div class="links">' + for _, link_data in ipairs(link_list) do + local link = link_data.link or link_data[1] or "#" + local text = link_data.text or link_data[2] or link + local external = link_data.external or false + if external then + html = html .. '<a target="_blank" href="' .. link .. '">' .. text .. '</a>' + else + html = html .. '<a href="' .. link .. '">' .. text .. '</a>' + end + end + html = html .. '</div>' + self:custom(html) + return self +end + +function M:lead(str) + str = str or "" + self:custom('<p class="lead">' .. str .. '</p>') + return self +end + +function M:small(str) + str = str or "" + self:custom('<div class="small">' .. str .. '</div>') + return self +end + +function M:highlight(str) + str = str or "" + self:custom('<span class="highlight">' .. str .. '</span>') + return self +end + function M:build() - return self.header .. table.concat(self.parts) .. self.footer + local header = self.header:gsub("{{TITLE}}", self.title or "Document") + return header .. table.concat(self.parts) .. self.footer end M.__tostring = function(self) diff --git a/core/std.lua b/core/std.lua @@ -16,4 +16,194 @@ function M.site_version() return "" end -return M -\ No newline at end of file +function M.a(link, str) + return "<a href=\"" .. link .. "\">" .. str .. "</a>" +end + +function M.external(link, str) + return "<a target=\"_blank\" href=\"" .. link .. "\">" .. str .. "</a>" +end + +function M.note(str) + return '<div class="note">' .. str .. '</div>' +end + +function M.muted(str) + return '<div class="muted">' .. str .. '</div>' +end + +function M.h1(str) + return "<h1>" .. (str or "") .. "</h1>" +end + +function M.h2(str) + return "<h2>" .. (str or "") .. "</h2>" +end + +function M.h3(str) + return "<h3>" .. (str or "") .. "</h3>" +end + +function M.h4(str) + return "<h4>" .. (str or "") .. "</h4>" +end + +function M.h5(str) + return "<h5>" .. (str or "") .. "</h5>" +end + +function M.h6(str) + return "<h6>" .. (str or "") .. "</h6>" +end + +function M.p(str) + return "<p>" .. (str or "") .. "</p>" +end + +function M.code(str) + return "<code>" .. (str or "") .. "</code>" +end + +function M.pre(str) + return "<pre><code>" .. (str or "") .. "</code></pre>" +end + +function M.ul(items) + items = items or {} + local html = "<ul>" + for _, item in ipairs(items) do + html = html .. "<li>" .. tostring(item) .. "</li>" + end + html = html .. "</ul>" + return html +end + +function M.ol(items) + items = items or {} + local html = "<ol>" + for _, item in ipairs(items) do + html = html .. "<li>" .. tostring(item) .. "</li>" + end + html = html .. "</ol>" + return html +end + +function M.blockquote(str) + return "<blockquote>" .. (str or "") .. "</blockquote>" +end + +function M.hr() + return "<hr>" +end + +function M.img(src, alt) + src = src or "" + alt = alt or "" + return '<img src="' .. src .. '" alt="' .. alt .. '">' +end + +function M.strong(str) + return "<strong>" .. (str or "") .. "</strong>" +end + +function M.em(str) + return "<em>" .. (str or "") .. "</em>" +end + +function M.br() + return "<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>" +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>" +end + +-- HTML escaping utility +function M.escape(str) + str = tostring(str or "") + str = str:gsub("&", "&amp;") + str = str:gsub("<", "&lt;") + str = str:gsub(">", "&gt;") + str = str:gsub('"', "&quot;") + str = str:gsub("'", "&#39;") + 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 + +-- Join array with separator +function M.join(arr, sep) + arr = arr or {} + sep = sep or ", " + local result = {} + for _, v in ipairs(arr) do + table.insert(result, tostring(v)) + end + return table.concat(result, sep) +end + +-- Trim whitespace +function M.trim(str) + str = tostring(str or "") + return str:match("^%s*(.-)%s*$") +end + +-- Table HTML generator +function M.table(headers, rows) + headers = headers or {} + rows = rows or {} + + local html = "<table><thead><tr>" + for _, header in ipairs(headers) do + html = html .. "<th>" .. tostring(header) .. "</th>" + end + html = html .. "</tr></thead><tbody>" + + for _, row in ipairs(rows) do + html = html .. "<tr>" + for _, cell in ipairs(row) do + html = html .. "<td>" .. tostring(cell) .. "</td>" + end + html = html .. "</tr>" + end + + html = html .. "</tbody></table>" + return html +end + +return M diff --git a/doc/Fes.toml b/doc/Fes.toml @@ -0,0 +1,9 @@ +[site] + +name = "doc" +version = "0.0.1" +authors = ["vx-clutch"] + +[fes] +version = "1.0.0" +CUSTOM_CSS = diff --git a/doc/www/index.lua b/doc/www/index.lua @@ -0,0 +1,38 @@ +local fes = require("fes") +local site = fes.site_builder() + +site.title = "Fes Documentation" + +site:h1("Fes Documentation") +site:note([[ +This is the documentation for the Fes +Microframework. This documentation serves as +a starting point and in its current state is +not comprehensive. Furthermore, you should +note that Fes is not production grade or +stable, use at your own caution. +]]) + +site:muted("Before reading this you should consult the " .. fes.std.external("https://git.vxserver.dev/fSD/fes", "README")) + +local docs = {} + +local template = [[ +<span class="highlight">%s</span> %s +]] +function docs:func(fn, desc) + table.insert(self, string.format(template, fn, desc)) + return self +end + +docs:func("fes.site_builder", "returns a site object, a required element for this framework.") +docs:func("site:h1", "adds a h1 to the site object.") +docs:func("site:h2", "adds a h2 to the site object.") +docs:func("site:h3", "adds a h3 to the site object.") +docs:func("site:h4", "adds a h4 to the site object.") +docs:func("site:h5", "adds a h5 to the site object.") +docs:func("site:h6", "adds a h6 to the site object.") + +site:note(table.concat(docs, "\n<br><br>\n")) + +return site diff --git a/main.go b/main.go @@ -21,10 +21,8 @@ import ( //go:embed core/builtin.lua var builtinLua string - //go:embed core/markdown.lua var markdownLua string - //go:embed core/std.lua var stdLua string @@ -44,7 +42,6 @@ type MyConfig struct { var port = flag.Int("p", 3000, "set the server port") -// markdownToHTML converts markdown text to HTML func markdownToHTML(mdText string) string { extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) @@ -64,7 +61,6 @@ func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) { L.PreloadModule("fes", func(L *lua.LState) int { mod := L.NewTable() - // Load core modules from embedded files coreModules := map[string]string{ "builtin": builtinLua, "markdown": markdownLua, @@ -92,7 +88,7 @@ func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) { mod.RawSetString(modName, tbl) } } - // Pass config to Lua + if cfg != nil { configTable := L.NewTable() siteTable := L.NewTable() @@ -109,7 +105,7 @@ func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) { configTable.RawSetString("fes", fesTable) mod.RawSetString("config", configTable) } - // Register markdown_to_html function + mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { mdText := L.ToString(1) html := markdownToHTML(mdText) @@ -198,10 +194,8 @@ CUSTOM_CSS = } func fixMalformedToml(content string) string { - // Fix lines like "CUSTOM_CSS =" (with no value) to "CUSTOM_CSS = \"\"" re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`) return re.ReplaceAllStringFunc(content, func(match string) string { - // Extract the key name parts := strings.Split(strings.TrimSpace(match), "=") if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" { key := strings.TrimSpace(parts[0]) @@ -217,7 +211,6 @@ func startServer(dir string) error { return err } - // Fix malformed TOML before parsing docStr := fixMalformedToml(string(doc)) var cfg MyConfig @@ -226,15 +219,43 @@ func startServer(dir string) error { return fmt.Errorf("failed to parse Fes.toml: %w", err) } - luaPath := filepath.Join(dir, "www", "index.lua") - data, err := loadLua(dir, luaPath, &cfg) + wwwDir := filepath.Join(dir, "www") + + entries, err := os.ReadDir(wwwDir) if err != nil { - return err + return fmt.Errorf("failed to read www directory: %w", err) } - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(data)) - }) - fmt.Printf("App running at:\n - Local: http://localhost:%d/\n", *port) + + 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 + } + } + } + + for route, luaPath := range routes { + func(rt string, lp string) { + http.HandleFunc(rt, func(w http.ResponseWriter, r *http.Request) { + data, err := loadLua(dir, lp, &cfg) + if err != nil { + http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError) + return + } + w.Write([]byte(data)) + }) + }(route, luaPath) + } + + fmt.Printf("Server is running on http://localhost:%d\n", *port) + return http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) } @@ -242,7 +263,7 @@ func main() { flag.Parse() if len(os.Args) < 3 { fmt.Println("Usage: fes <command> <project_dir>") - fmt.Println("Commands: new, serve") + fmt.Println("Commands: new, run") os.Exit(1) }