fes

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

commit 705911ac9d9006479597fd62b4f69c8b91068981
parent e2d31aa734825a2cf50a6bf0c8095d5a185944cf
Author: vx-clutch <[email protected]>
Date:   Fri, 28 Nov 2025 15:56:49 -0500

alpha p1

Diffstat:
MTODO | 3---
Mcore/builtin.lua | 428+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Dcore/console.lua | 19-------------------
Acore/log.lua | 7+++++++
Dcore/markdown.lua | 19-------------------
Mcore/std.lua | 41++++++++++++++++++++++++++++++++++++++---
Aexamples/canonical/Fes.toml | 5+++++
Aexamples/canonical/include/header.lua | 16++++++++++++++++
Aexamples/canonical/www/index.lua | 17+++++++++++++++++
Aexamples/hello-world/Fes.toml | 5+++++
Aexamples/hello-world/www/index.lua | 9+++++++++
Aexamples/multi-page/Fes.toml | 5+++++
Aexamples/multi-page/www/index.lua | 15+++++++++++++++
Aexamples/multi-page/www/page1.lua | 15+++++++++++++++
Aexamples/multi-page/www/page2.lua | 15+++++++++++++++
Mmain.go | 278++++---------------------------------------------------------------------------
Asrc/config/config.go | 14++++++++++++++
Asrc/new/new.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/server/server.go | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/Fes.toml | 9---------
Dtest/www/index.lua | 11-----------
21 files changed, 773 insertions(+), 462 deletions(-)

diff --git a/TODO b/TODO @@ -1,5 +1,2 @@ Make this static ( cannot find core.std everywhere ) -Add hotreloading Add an interval element -Compare bin version to Fes.toml -Add ways to interact with left and right diff --git a/core/builtin.lua b/core/builtin.lua @@ -27,23 +27,85 @@ function M.fes(header, footer) <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>{{TITLE}}</title> <style> -html,body{ - min-height:100%; - background:#0f1113; +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{ - margin:0; - padding:36px; - font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; - background:#0f1113; - color:#e6eef3; - line-height:1.45 + +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; } -.container{max-width:1100px;margin:0 auto} -h1,h2,h3,h4,h5,h6{ - font-weight:600; - margin:0 0 12px 0 +.hidden { + color: #dfe9ee; + text-decoration: none; +} + +a:hover { + text-decoration: underline; } summary { @@ -51,172 +113,265 @@ summary { } details { - background:#17191b; - border:1px solid rgba(255,255,255,.06); - border-radius:4px; - padding:12px 14px; - margin:14px 0; + background: #1a1c20; + border: 1px solid rgba(255,255,255,.06); + border-radius: 4px; + padding: 14px 16px; + margin: 16px 0; } details summary { list-style: none; - cursor: pointer; - font-weight:600; - color:#e6eef3; + font-weight: 600; + color: #e6eef3; + display: flex; + align-items: center; } details summary::-webkit-details-marker { - display:none; + display: none; } details summary::before { - content:"▸"; - display:inline-block; - margin-right:8px; - transition:transform .15s ease; - color:#68a6ff; + content: "▸"; + margin-right: 8px; + transition: transform .15s ease; + color: #68a6ff; +} + +details[open] summary::before { + transform: rotate(90deg); } summary::after { - content: "Expand description"; + content: "Expand"; + margin-left: auto; + font-size: 13px; + color: #9aa6b1; } details[open] summary::after { - content: "Close description"; + content: "Collapse"; } details > *:not(summary) { - margin-top:10px; + 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; } -h1{font-size:40px;margin-bottom:18px;font-weight:700} -h2{font-size:32px;margin:24px 0 14px} -h3{font-size:26px;margin:20px 0 12px} -h4{font-size:20px;margin:16px 0 10px} -h5{font-size:16px;margin:14px 0 8px} -h6{font-size:14px;margin:12px 0 6px;color:#9aa6b1} +.lead { + font-size: 15px; + margin-top: 8px; +} -p{margin:12px 0} +.callout { + display: block; + margin: 12px 0; +} -a{ - color:#68a6ff; - text-decoration:none +.small { + font-size: 13px; + color: #9aa6b1; + margin-top: 6px; } -a:hover{text-decoration:underline} -.note,pre,code{ - background:#17191b; - border:1px solid rgba(255,255,255,.06) +.highlight { + font-weight: 700; + color: #cde7ff; } -.note{ - padding:18px; - border-radius:4px; - margin:12px 0 26px; - color:#dfe9ee + +ul, ol { + margin: 14px 0; + padding-left: 26px; } -.note strong{color:#f0f6f8} -.muted{color:#9aa6b1} +.tl { + display: grid; + grid-template-columns: repeat(auto-fill, 200px); + gap: 15px; + list-style-type: none; + padding: 0; + margin: 0; + justify-content: start; +} -.lead{font-size:15px;margin-top:8px} -.callout{display:block;margin:10px 0} -.small{font-size:13px;color:#9aa6b1;margin-top:6px} -.highlight{font-weight:700;color:#cde7ff} +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; +} -ul,ol{margin:12px 0;padding-left:24px} -li{margin:6px 0} +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; +} -code{ - padding:2px 6px; - border-radius:3px; - font-family:"SF Mono",Monaco,"Cascadia Code","Roboto Mono",Consolas,"Courier New",monospace; - font-size:.9em; - color:#cde7ff +pre code { + background: none; + border: none; + padding: 0; + font-size: inherit; } -pre{ - 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 + +blockquote { + border-left: 3px solid #68a6ff; + padding-left: 18px; + margin: 14px 0; + color: #dfe9ee; + font-style: italic; } -pre code{ - background:none; - border:none; - padding:0; - font-size:inherit + +hr { + border: 0; + border-top: 1px solid rgba(255,255,255,.1); + margin: 26px 0; } -blockquote{ - border-left:3px solid #68a6ff; - padding-left:18px; - margin:12px 0; - color:#dfe9ee; - font-style:italic +img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 14px 0; } -hr{ - border:none; - border-top:1px solid rgba(255,255,255,.1); - margin:24px 0 +table { + width: 100%; + border-collapse: collapse; + margin: 14px 0; } -img{ - max-width:100%; - height:auto; - border-radius:4px; - margin:12px 0 +th, td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid rgba(255,255,255,.06); } -table{ - width:100%; - border-collapse:collapse; - margin:12px 0 +th { + background: #1a1c20; + font-weight: 600; + color: #f0f6f8; } -th,td{ - padding:10px 14px; - text-align:left; - border-bottom:1px solid rgba(255,255,255,.06) + +tr:hover { + background: rgba(255,255,255,.02); } -th{ - background:#17191b; - font-weight:600; - color:#f0f6f8 + +.divider { + margin: 26px 0; + height: 1px; + background: rgba(255,255,255,.1); } -tr:hover{background:rgba(255,255,255,.02)} -.divider{ - margin:24px 0; - height:1px; - background:rgba(255,255,255,.1) +.section { + margin-top: 36px; } -.section{margin-top:32px} +.links { + margin: 12px 0; +} -.links{margin:10px 0} -.links a{ - display:inline-block; - margin:0 12px 6px 0 +.links a { + display: inline-block; + margin: 0 14px 6px 0; } -strong,b{font-weight:600;color:#f0f6f8} -em,i{font-style:italic} +strong, b { + font-weight: 600; + color: #f0f6f8; +} -.footer { - background: #1a1c20; - margin-top: 36px; - padding: 18px 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; +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; /* <— Makes it reappear */ + margin-bottom: 0; +} </style> </head> <body> @@ -304,6 +459,13 @@ function M:a(link, str) return self end +function M:ha(link, str) + link = link or "example.com" + str = str or link + table.insert(self.parts, "<a class=\"hidden\" href=\"" .. link .. "\">" .. str .. "</a>") + return self +end + function M:external(link, str) link = link or "example.com" str = str or link @@ -454,26 +616,30 @@ end function M:lead(str) str = str or "" - self:custom('<p class="lead">' .. str .. '</p>') + self:custom(std.small(str)) return self end function M:small(str) str = str or "" - self:custom('<div class="small">' .. str .. '</div>') + self:custom(std.small(str)) return self end function M:highlight(str) str = str or "" - self:custom('<span class="highlight">' .. str .. '</span>') + self:custom(std.highlight(str)) return self end +function M:banner(str) + self:custom(std.banner(str)) +end + function M:build() local header = self.header:gsub("{{TITLE}}", self.title or "Document") local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") - return header .. table.concat(self.parts) .. footer + return header .. table.concat(self.parts, "\n") .. footer end M.__tostring = function(self) diff --git a/core/console.lua b/core/console.lua @@ -1,19 +0,0 @@ -local M = {} - -function M.log(fmt, ...) - print(string.format("[%12f] ", os.clock()) .. string.format(fmt, ...)) -end - -function M.info(fmt, ...) - print("INFO: " .. string.format(fmt, ...)) -end - -function M.warn(fmt, ...) - print("WARN: " .. string.format(fmt, ...)) -end - -function M.error(fmt, ...) - print("ERROR: " .. string.format(fmt, ...)) -end - -return M diff --git a/core/log.lua b/core/log.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.printl(fmt, ...) + print(string.format(fmt, ...)) +end + +return M diff --git a/core/markdown.lua b/core/markdown.lua @@ -1,19 +0,0 @@ -local M = {} - --- Markdown to HTML conversion function --- Uses the Go backend markdown parser -function M.to_html(markdown_text) - markdown_text = markdown_text or "" - - -- Get the fes module - local fes_mod = package.loaded.fes - if fes_mod and fes_mod.markdown_to_html then - return fes_mod.markdown_to_html(markdown_text) - end - - -- Fallback: return error message if Go function not available - return "<p>Error: markdown_to_html function not available</p>" -end - -return M - diff --git a/core/std.lua b/core/std.lua @@ -17,9 +17,17 @@ function M.site_version() end function M.a(link, str) + link = link or "https://example.com" + str = str or link return "<a href=\"" .. link .. "\">" .. str .. "</a>" end +function M.ha(link, str) + link = link or "https://example.com" + str = str or link + return "<a class=\"hidden\" href=\"" .. link .. "\">" .. str .. "</a>" +end + function M.external(link, str) return "<a target=\"_blank\" href=\"" .. link .. "\">" .. str .. "</a>" end @@ -92,6 +100,16 @@ function M.ol(items) return html end +function M.tl(items) + items = items or {} + local html = '<ul class="tl">' + for _, item in ipairs(items) do + html = html .. "<li>" .. tostring(item) .. "</li>" + end + html = html .. "</ul>" + return html +end + function M.blockquote(str) return "<blockquote>" .. (str or "") .. "</blockquote>" end @@ -191,13 +209,13 @@ 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 @@ -205,7 +223,7 @@ function M.table(headers, rows) end html = html .. "</tr>" end - + html = html .. "</tbody></table>" return html end @@ -215,7 +233,24 @@ function M.copyright() end function M.highlight(str) + str = str or "" return '<span class="highlight">' .. str .. "</span>" end +function M.banner(str) + str = str or "" + return '<div class="banner">' .. str .. "</div>" +end + +function M.center(str) + str = str or "" + return '<div class="center">' .. str .. "</div>" +end + +function M.nav(link, str) + link = link or "example.com" + str = str or link + return '<a class="nav" href="' .. link .. '">' .. str .. "</a>" +end + return M diff --git a/examples/canonical/Fes.toml b/examples/canonical/Fes.toml @@ -0,0 +1,5 @@ +[app] + +name = "canonical" +version = "0.0.1" +authors = ["vx-clutch"] diff --git a/examples/canonical/include/header.lua b/examples/canonical/include/header.lua @@ -0,0 +1,16 @@ +local header = {} + +header.render = function(std) + return table.concat({ + std.center(std.h1("Canonical")), + std.center(table.concat({ + std.nav("example"), + std.nav("example"), + std.nav("example"), + std.nav("example"), + std.nav("example"), + })) + }) +end + +return header diff --git a/examples/canonical/www/index.lua b/examples/canonical/www/index.lua @@ -0,0 +1,17 @@ +local fes = require("fes") +local std = fes.std + +local site = fes.fes() + +site.title = "Canonical" +site.copyright = std.copyright() .. " " .. std.external("https://git.vxserver.dev/fSD", "fSD") + +site:banner(fes.app.header.render(std)) + +site:note(table.concat({ + std.h1("Canonical"), + std.p("This is the example for the canonical 'fes' site, by canonical is meant a format and " .. std.external("https://git.vxserver.dev/fSD/fes/src/branch/master/examples/canonical/www/index.lua", "code") .. " that resembles the typical use case of the Microframework"), + std.p("This page also serves as a test for the integrity of a 'fes' build, given that it uses plenty crucial features to show everything from the HTML to CSS as well as the interactivity of certain elements."), +})) + +return site diff --git a/examples/hello-world/Fes.toml b/examples/hello-world/Fes.toml @@ -0,0 +1,5 @@ +[app] + +name = "hello-world" +version = "0.0.1" +authors = ["vx-clutch"] diff --git a/examples/hello-world/www/index.lua b/examples/hello-world/www/index.lua @@ -0,0 +1,9 @@ +local fes = require("fes") +local site = fes.fes() + +site.title = "Hello, World!" +site.copyright = fes.std.copyright() .. " " .. fes.std.external("https://git.vxserver.dev/fSD", "fSD") + +site:h1("Hello, World!") + +return site diff --git a/examples/multi-page/Fes.toml b/examples/multi-page/Fes.toml @@ -0,0 +1,5 @@ +[app] + +name = "multi-page" +version = "0.0.1" +authors = ["vx-clutch"] diff --git a/examples/multi-page/www/index.lua b/examples/multi-page/www/index.lua @@ -0,0 +1,15 @@ +local fes = require("fes") +local site = fes.fes() + +site.title = "Home" +site.copyright = fes.std.copyright() .. " " .. fes.std.external("https://git.vxserver.dev/fSD", "fSD") + +site:h1("Home") +site:note( + fes.std.ul({ + fes.std.a("page1"), + fes.std.a("page2"), + }) +) + +return site diff --git a/examples/multi-page/www/page1.lua b/examples/multi-page/www/page1.lua @@ -0,0 +1,15 @@ +local fes = require("fes") +local site = fes.fes() + +site.title = "Page 1" +site.copyright = fes.std.copyright() .. " " .. fes.std.external("https://git.vxserver.dev/fSD", "fSD") + +site:h1("Page 1") +site:note( + fes.std.ul({ + fes.std.a("/", "home"), + fes.std.a("page2"), + }) +) + +return site diff --git a/examples/multi-page/www/page2.lua b/examples/multi-page/www/page2.lua @@ -0,0 +1,15 @@ +local fes = require("fes") +local site = fes.fes() + +site.title = "Page 2" +site.copyright = fes.std.copyright() .. " " .. fes.std.external("https://git.vxserver.dev/fSD", "fSD") + +site:h1("Page 2") +site:note( + fes.std.ul({ + fes.std.a("/", "home"), + fes.std.a("page1"), + }) +) + +return site diff --git a/main.go b/main.go @@ -1,285 +1,29 @@ package main import ( + "embed" _ "embed" "flag" "fmt" - "net/http" "os" - "os/exec" - "os/user" - "path/filepath" - "regexp" - "strings" - "time" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - "github.com/pelletier/go-toml/v2" - lua "github.com/yuin/gopher-lua" + "fes/src/config" + "fes/src/new" + "fes/src/server" ) -//go:embed core/builtin.lua -var builtinLua string +//go:embed core/* +var core embed.FS -//go:embed core/markdown.lua -var markdownLua string - -//go:embed core/std.lua -var stdLua string - -//go:embed core/console.lua -var consoleLua string - -const version = "1.0.0" - -type MyConfig struct { - Site struct { - Name string `toml:"name"` - Version string `toml:"version"` - Authors []string `toml:"authors"` - } `toml:"site"` - Fes struct { - Version string `toml:"version"` - CUSTOM_CSS string `toml:"CUSTOM_CSS,omitempty"` - } `toml:"fes"` -} - -var port = flag.Int("p", 3000, "set the server port") - -func markdownToHTML(mdText string) string { - extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock - p := parser.NewWithExtensions(extensions) - doc := p.Parse([]byte(mdText)) - - htmlFlags := html.CommonFlags | html.HrefTargetBlank - opts := html.RendererOptions{Flags: htmlFlags} - renderer := html.NewRenderer(opts) - - return string(markdown.Render(doc, renderer)) -} - -func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) { - L := lua.NewState() - defer L.Close() - - L.PreloadModule("core.std", func(L *lua.LState) int { - if err := L.DoString(stdLua); err != nil { - panic(err) - } - L.Push(L.Get(-1)) - return 1 - }) - - L.PreloadModule("fes", func(L *lua.LState) int { - mod := L.NewTable() - - coreModules := map[string]string{ - "builtin": builtinLua, - "markdown": markdownLua, - "std": stdLua, - "console": consoleLua, - } - - for modName, luaCode := range coreModules { - if err := L.DoString(luaCode); err != nil { - fmt.Println("error loading", modName, ":", err) - continue - } - val := L.Get(-1) - L.Pop(1) - tbl, ok := val.(*lua.LTable) - if !ok { - t := L.NewTable() - t.RawSetString("value", val) - tbl = t - } - if modName == "builtin" { - tbl.ForEach(func(key, value lua.LValue) { - mod.RawSet(key, value) - }) - } else { - mod.RawSetString(modName, tbl) - } - } - - if cfg != nil { - configTable := L.NewTable() - siteTable := L.NewTable() - siteTable.RawSetString("version", lua.LString(cfg.Site.Version)) - siteTable.RawSetString("name", lua.LString(cfg.Site.Name)) - authorsTable := L.NewTable() - for i, author := range cfg.Site.Authors { - authorsTable.RawSetInt(i+1, lua.LString(author)) - } - siteTable.RawSetString("authors", authorsTable) - configTable.RawSetString("site", siteTable) - fesTable := L.NewTable() - fesTable.RawSetString("version", lua.LString(cfg.Fes.Version)) - configTable.RawSetString("fes", fesTable) - mod.RawSetString("config", configTable) - } - - mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { - mdText := L.ToString(1) - html := markdownToHTML(mdText) - L.Push(lua.LString(html)) - return 1 - })) - L.Push(mod) - return 1 - }) - - if err := L.DoFile(entry); err != nil { - return "", err - } - - top := L.GetTop() - if top == 0 { - fmt.Println("warning: no return value from Lua file") - return "", nil - } - - resultVal := L.Get(-1) - L.SetGlobal("__fes_result", resultVal) - if err := L.DoString("return tostring(__fes_result)"); err != nil { - L.GetGlobal("__fes_result") - if s := L.ToString(-1); s != "" { - return s, nil - } - return "", nil - } - if s := L.ToString(-1); s != "" { - return s, nil - } - return "", nil -} - -func getName() string { - out, err := exec.Command("git", "config", "user.name").Output() - if err == nil { - s := strings.TrimSpace(string(out)) - if s != "" { - return s - } - } - u, err := user.Current() - if err == nil && u.Username != "" { - return u.Username - } - return "" -} - -func newProject(dir string) error { - if err := os.MkdirAll(filepath.Join(dir, "www"), 0755); err != nil { - return err - } - indexLua := filepath.Join(dir, "www", "index.lua") - if _, err := os.Stat(indexLua); os.IsNotExist(err) { - content := `local fes = require("fes") -local site = fes.site_builder() - -site:h1("Hello, World!") - -return site -` - if err := os.WriteFile(indexLua, []byte(content), 0644); err != nil { - return err - } - } - indexFes := filepath.Join(dir, "Fes.toml") - if _, err := os.Stat(indexFes); os.IsNotExist(err) { - content := fmt.Sprintf(`[site] - -name = "%s" -version = "0.0.1" -authors = ["%s"] - -[fes] -version = "%s" -CUSTOM_CSS = -`, dir, getName(), version) - if err := os.WriteFile(indexFes, []byte(content), 0644); err != nil { - return err - } - } - fmt.Println("Created new project at", dir) - return nil -} - -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 startServer(dir string) error { - doc, err := os.ReadFile(filepath.Join(dir, "Fes.toml")) - if err != nil { - return err - } - - docStr := fixMalformedToml(string(doc)) - - var cfg MyConfig - err = toml.Unmarshal([]byte(docStr), &cfg) - if err != nil { - return fmt.Errorf("failed to parse Fes.toml: %w", err) - } - - wwwDir := filepath.Join(dir, "www") - - entries, err := os.ReadDir(wwwDir) - if err != nil { - return fmt.Errorf("failed to read www directory: %w", err) - } - - 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) { - fmt.Printf("[%s] LOAD /%s\n", time.Now().Format(time.RFC1123), lp) - 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) +func init() { + config.Port = flag.Int("p", 3000, "set the server port") + config.Core = core } func main() { flag.Parse() if len(os.Args) < 3 { fmt.Println("Usage: fes <command> <project_dir>") - fmt.Println("Commands: new, run") os.Exit(1) } @@ -288,11 +32,11 @@ func main() { switch cmd { case "new": - if err := newProject(dir); err != nil { + if err := new.Project(dir); err != nil { panic(err) } case "run": - if err := startServer(dir); err != nil { + if err := server.Start(dir); err != nil { panic(err) } default: diff --git a/src/config/config.go b/src/config/config.go @@ -0,0 +1,14 @@ +package config + +import "embed" + +var Core embed.FS +var Port *int + +type MyConfig struct { + App struct { + Name string `toml:"name"` + Version string `toml:"version"` + Authors []string `toml:"authors"` + } `toml:"app"` +} diff --git a/src/new/new.go b/src/new/new.go @@ -0,0 +1,57 @@ +package new + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "strings" +) + +func getName() string { + out, err := exec.Command("git", "config", "user.name").Output() + if err == nil { + s := strings.TrimSpace(string(out)) + if s != "" { + return s + } + } + u, err := user.Current() + if err == nil && u.Username != "" { + return u.Username + } + return "" +} + +func Project(dir string) error { + if err := os.MkdirAll(filepath.Join(dir, "www"), 0755); err != nil { + return err + } + indexLua := filepath.Join(dir, "www", "index.lua") + if _, err := os.Stat(indexLua); os.IsNotExist(err) { + content := `local fes = require("fes") +local site = fes.fes() + +site:h1("Hello, World!") + +return site +` + if err := os.WriteFile(indexLua, []byte(content), 0644); err != nil { + return err + } + } + indexFes := filepath.Join(dir, "Fes.toml") + if _, err := os.Stat(indexFes); os.IsNotExist(err) { + content := fmt.Sprintf(`[app] + +name = "%s" +version = "0.0.1" +authors = ["%s"]`, dir, getName()) + if err := os.WriteFile(indexFes, []byte(content), 0644); err != nil { + return err + } + } + fmt.Println("Created new project at", dir) + return nil +} diff --git a/src/server/server.go b/src/server/server.go @@ -0,0 +1,247 @@ +package server + +import ( + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/pelletier/go-toml/v2" + lua "github.com/yuin/gopher-lua" + + "fes/src/config" +) + +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) + doc := p.Parse([]byte(mdText)) + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + return string(markdown.Render(doc, renderer)) +} + +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() { + continue + } + name := e.Name() + if !strings.HasSuffix(name, ".lua") { + continue + } + base := strings.TrimSuffix(name, ".lua") + path := filepath.Join(includeDir, name) + if err := L.DoFile(path); err != nil { + fmt.Printf("Failed to load %s: %v\n", path, err) + continue + } + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok { + tbl = L.NewTable() + } + app.RawSetString(base, tbl) + } + return app +} + +func loadLua(luaDir string, entry string, cfg *config.MyConfig) (string, error) { + L := lua.NewState() + defer L.Close() + + rdents, err := fs.ReadDir(config.Core, "core") + if err == nil { + for _, de := range rdents { + if de.IsDir() { + continue + } + name := de.Name() + if !strings.HasSuffix(name, ".lua") { + continue + } + path := filepath.Join("core", name) + data, err := config.Core.ReadFile(path) + if err != nil { + continue + } + if err := L.DoString(string(data)); err != nil { + continue + } + } + } + + L.PreloadModule("core.std", func(L *lua.LState) int { + data, err := config.Core.ReadFile("core/std.lua") + if err != nil { + panic(err) + } + if err := L.DoString(string(data)); err != nil { + panic(err) + } + L.Push(L.Get(-1)) + return 1 + }) + + L.PreloadModule("fes", func(L *lua.LState) int { + mod := L.NewTable() + coreModules := []string{} + if ents, err := fs.ReadDir(config.Core, "core"); err == nil { + for _, e := range ents { + if e.IsDir() { + continue + } + n := e.Name() + if strings.HasSuffix(n, ".lua") { + coreModules = append(coreModules, strings.TrimSuffix(n, ".lua")) + } + } + } + for _, modName := range coreModules { + path := filepath.Join("core", modName+".lua") + data, err := config.Core.ReadFile(path) + if err != nil { + continue + } + if err := L.DoString(string(data)); err != nil { + continue + } + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok || tbl == nil { + tbl = L.NewTable() + } + if modName == "builtin" { + tbl.ForEach(func(k, v lua.LValue) { + mod.RawSet(k, v) + }) + } else { + mod.RawSetString(modName, tbl) + } + } + + includeDir := filepath.Join(luaDir, "include") + appTbl := loadIncludeModules(L, includeDir) + mod.RawSetString("app", appTbl) + + if cfg != nil { + siteTable := L.NewTable() + siteTable.RawSetString("version", lua.LString(cfg.App.Version)) + siteTable.RawSetString("name", lua.LString(cfg.App.Name)) + authorsTable := L.NewTable() + for i, author := range cfg.App.Authors { + authorsTable.RawSetInt(i+1, lua.LString(author)) + } + siteTable.RawSetString("authors", authorsTable) + mod.RawSetString("site", siteTable) + } + + mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { + mdText := L.ToString(1) + html := markdownToHTML(mdText) + L.Push(lua.LString(html)) + return 1 + })) + + L.Push(mod) + return 1 + }) + + if err := L.DoFile(entry); err != nil { + return "", err + } + + top := L.GetTop() + if top == 0 { + return "", nil + } + resultVal := L.Get(-1) + L.SetGlobal("__fes_result", resultVal) + if err := L.DoString("return tostring(__fes_result)"); err != nil { + L.GetGlobal("__fes_result") + if s := L.ToString(-1); s != "" { + return s, nil + } + return "", nil + } + if s := L.ToString(-1); s != "" { + return s, nil + } + return "", nil +} + +func Start(dir string) error { + doc, err := os.ReadFile(filepath.Join(dir, "Fes.toml")) + if err != nil { + return err + } + docStr := fixMalformedToml(string(doc)) + var cfg config.MyConfig + err = toml.Unmarshal([]byte(docStr), &cfg) + if err != nil { + return fmt.Errorf("failed to parse Fes.toml: %w", err) + } + + wwwDir := filepath.Join(dir, "www") + entries, err := os.ReadDir(wwwDir) + if err != nil { + return fmt.Errorf("failed to read www directory: %w", err) + } + + 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) { + fmt.Printf("-> %s\n", lp) + 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", *config.Port) + return http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil) +} diff --git a/test/Fes.toml b/test/Fes.toml @@ -1,9 +0,0 @@ -[site] - -name = "test" -version = "0.0.1" -authors = ["vx-clutch"] - -[fes] -version = "1.0.0" -CUSTOM_CSS = diff --git a/test/www/index.lua b/test/www/index.lua @@ -1,11 +0,0 @@ -local fes = require("fes") -local site = fes() - -site:h1("<h1>") -site:h2("<h2>") -site:h3("<h3>") -site:h4("<h4>") -site:h5("<h5>") -site:h6("<h6>") - -return site