commit 705911ac9d9006479597fd62b4f69c8b91068981
parent e2d31aa734825a2cf50a6bf0c8095d5a185944cf
Author: vx-clutch <[email protected]>
Date: Fri, 28 Nov 2025 15:56:49 -0500
alpha p1
Diffstat:
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 "© 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