fes

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

commit e4eb7d62e40592e25ab3a236ea227346ba8bad05
parent 9d7dbc31ca2da16ffdd26d0f68d6934ed09861e6
Author: vx-clutch <[email protected]>
Date:   Fri, 26 Dec 2025 10:06:23 -0500

large changes

Diffstat:
DDockerfile | 9---------
AMakefile | 19+++++++++++++++++++
Dcore/builtin.lua | 348-------------------------------------------------------------------------------
Dcore/util.lua | 14--------------
Rcore/dkjson.lua -> lib/dkjson.lua | 0
Alib/fes.lua | 348+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcore/std.lua -> lib/std.lua | 0
Rcore/symbol.lua -> lib/symbol.lua | 0
Alib/util.lua | 14++++++++++++++
Mmain.go | 24++++++++++++++++--------
Amodules/config/config.go | 22++++++++++++++++++++++
Amodules/doc/doc.go | 22++++++++++++++++++++++
Rsrc/new/new.go -> modules/new/new.go | 0
Amodules/server/server.go | 442+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/server/util.go | 45+++++++++++++++++++++++++++++++++++++++++++++
Amodules/ui/ui.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/version/version.go | 22++++++++++++++++++++++
Dsrc/config/config.go | 21---------------------
Dsrc/doc/doc.go | 22----------------------
Dsrc/server/server.go | 484-------------------------------------------------------------------------------
Dsrc/ui/ui.go | 57---------------------------------------------------------
Dsrc/version/version.go | 15---------------
22 files changed, 1007 insertions(+), 978 deletions(-)

diff --git a/Dockerfile b/Dockerfile @@ -1,9 +0,0 @@ -FROM alpine:3.20 - -ARG SITE - -RUN echo "https://git.vxserver.dev/api/packages/fSD/alpine/main/fports" >> /etc/apk/repositories - && apk update \ - && apk add --no-cache fes - -ENTRYPOINT ["fes", "run", ${SITE}] diff --git a/Makefile b/Makefile @@ -0,0 +1,19 @@ +GO ?= go + +.PHONY: build deps lint install + +all: build + +deps: + $(GO) mod download + +build: deps + $(GO) build -ldflags "-X fes/modules/version.gitCommit=$(shell git rev-parse --short HEAD)" -o fes + @echo "Fes is now built to ./fes" + +lint: + $(GO) vet ./... + $(GO) fmt ./... + +install: + $(GO) install fes diff --git a/core/builtin.lua b/core/builtin.lua @@ -1,348 +0,0 @@ -local std = require("core.std") - -local M = {} -M.__index = M - -function M.fes(header, footer) - local config = {} local site_config = {} - local fes_mod = package.loaded.fes - if fes_mod and fes_mod.config then - config = fes_mod.config - if config.site then - site_config = config.site - end - end - - if site_config.favicon then - site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">' - end - - local self = { - version = site_config.version, - title = site_config.title, - copyright = site_config.copyright, - favicon = site_config.favicon, - header = header or [[ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8"> -<meta name="viewport" content="width=device-width,initial-scale=1.0"> -{{FAVICON}} -<title>{{TITLE}}</title> -<style> -:root { - --bg: #f5f5f5; - --text: #111827; - --muted: #6b7280; - --link: #1a0dab; - --accent: #68a6ff; - --highlight: #004d99; - --note-bg: #ffffff; - --panel-bg: #ffffff; - --border: rgba(0,0,0,.1); - --table-head: #f3f4f6; - --code-color: #004d99; - --blockquote-border: #1a73e8; - --banner-bg: #ffffff; - --footer-bg: #ffffff; - --shadow: rgba(0,0,0,.08); -} - -@media (prefers-color-scheme: dark) { - :root { - --bg: #0f1113; - --text: #e6eef3; - --muted: #9aa6b1; - --link: #68a6ff; - --accent: #68a6ff; - --highlight: #cde7ff; - --note-bg: #1a1c20; - --panel-bg: #1a1c20; - --border: rgba(255,255,255,.06); - --table-head: #1a1c20; - --code-color: #cde7ff; - --blockquote-border: #68a6ff; - --banner-bg: #1a1c20; - --footer-bg: #1a1c20; - --shadow: rgba(0,0,0,.4); - } -} - -html, body { - min-height: 100%; - margin: 0; - padding: 0; - background: var(--bg); - color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { padding: 36px; } - -.container { max-width: 830px; margin: 0 auto; } - -.container > *:not(.banner) { margin: 28px 0; } - -h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; } - -h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; } - -h2 { font-size: 32px; margin: 26px 0 14px; } - -h3 { font-size: 26px; margin: 22px 0 12px; } - -h4 { font-size: 20px; margin: 18px 0 10px; } - -h5 { font-size: 16px; margin: 16px 0 8px; } - -h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); } - -p { margin: 14px 0; } - -a { color: var(--link); text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; } - -.hidden { color: var(--text); text-decoration: none; } - -a:hover { text-decoration: underline; } - -summary { cursor: pointer; } - -details { - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 4px; - padding: 14px 16px; - margin: 16px 0; -} - -details summary { - list-style: none; - font-weight: 600; - color: var(--text); - display: flex; - align-items: center; -} - -details summary::-webkit-details-marker { display: none; } - -details summary::before { - content: "▸"; - margin-right: 8px; - transition: transform .15s ease; - color: var(--accent); -} - -details[open] summary::before { transform: rotate(90deg); } - -summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); } - -details[open] summary::after { content: "Collapse"; } - -details > *:not(summary) { margin-top: 12px; } - -.note, pre, code { - background: var(--note-bg); - border: 1px solid var(--border); -} - -.note { - padding: 20px; - border-radius: 4px; - background: var(--note-bg); - border: 1px solid var(--border); - margin: 28px 0; - color: var(--text); -} - -.note strong { color: var(--text); } - -.muted { color: var(--muted); } - -.lead { font-size: 15px; margin-top: 8px; } - -.callout { display: block; margin: 12px 0; } - -.small { font-size: 13px; color: var(--muted); margin-top: 6px; } - -.highlight { font-weight: 700; color: var(--highlight); } - -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: var(--code-color); -} - -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 var(--blockquote-border); - padding-left: 18px; - margin: 14px 0; - color: var(--text); - font-style: italic; -} - -hr { border: 0; border-top: 1px solid rgba(0,0,0,.08); margin: 26px 0; } - -@media (prefers-color-scheme: dark) { - hr { border-top-color: rgba(255,255,255,.1); } -} - -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 var(--border); -} - -th { - background: var(--table-head); - font-weight: 600; - color: var(--text); -} - -tr:hover { background: rgba(0,0,0,0.02); } - -@media (prefers-color-scheme: dark) { - tr:hover { background: rgba(255,255,255,0.02); } -} - -.divider { margin: 26px 0; height: 1px; background: rgba(0,0,0,.08); } - -@media (prefers-color-scheme: dark) { - .divider { background: rgba(255,255,255,.1); } -} - -.section { margin-top: 36px; } - -.links { margin: 12px 0; } - -.links a { display: inline-block; margin: 0 14px 6px 0; color: var(--link); } - -strong, b { font-weight: 600; color: var(--text); } - -em, i { font-style: italic; } - -.center { display: flex; justify-content: center; align-items: center; } - -.banner { - width: 100%; - box-sizing: border-box; - text-align: center; - background: var(--banner-bg); - padding: 20px; - border: 1px solid var(--border); - border-bottom-right-radius: 8px; - border-bottom-left-radius: 8px; - color: var(--text); - margin: -36px 0 28px 0; - box-shadow: 0 0.2em 0.6em var(--shadow); -} - -.nav { margin-left: auto; margin-right: auto; } - -.nav a { color: var(--highlight); } - -.footer { - background: var(--footer-bg); - padding: 20px 0; - border-top: 1px solid rgba(0,0,0,.08); - font-size: 14px; - color: var(--muted); - display: flex; - justify-content: center; - align-items: center; - gap: 24px; - margin-top: 28px !important; - margin-bottom: 0; -} - -.left { text-align: left; float: left; } - -.right { text-align: right; float: right; } -</style> -</head> -<body> -<div class="container"> -]], - footer = footer or [[ -<footer class="footer"> -<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a> -<a href="https://www.lua.org/" target="_blank">Lua Powered</a> -<a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a> -<p>{{COPYRIGHT}}</p> -</footer> -</div> -</body> -</html> -]], - parts = {} - } - - return setmetatable(self, M) -end - -function M:custom(str) - table.insert(self.parts, str) - return self -end - -for name, func in pairs(std) do - if type(func) == "function" then - M[name] = function(self, ...) - local result = func(...) - table.insert(self.parts, result) - return self - end - end -end - -function M:build() - local header = self.header - header = header:gsub("{{TITLE}}", self.title or "Document") - local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">') - header = header:gsub("{{FAVICON}}", favicon_html or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]) - local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") - return header .. table.concat(self.parts, "\n") .. footer -end - -M.__tostring = function(self) - return self:build() -end - -return M diff --git a/core/util.lua b/core/util.lua @@ -1,14 +0,0 @@ -local std = require("core.std") -local symbol = require("core.symbol") - -local M = {} - -function M.cc(tbl) - return table.concat(tbl) -end - -function M.copyright(link, holder) - return symbol.copyright .. " " .. std.external(link, holder) -end - -return M diff --git a/core/dkjson.lua b/lib/dkjson.lua diff --git a/lib/fes.lua b/lib/fes.lua @@ -0,0 +1,348 @@ +local std = require("lib.std") + +local M = {} +M.__index = M + +function M.fes(header, footer) + local config = {} local site_config = {} + local fes_mod = package.loaded.fes + if fes_mod and fes_mod.config then + config = fes_mod.config + if config.site then + site_config = config.site + end + end + + if site_config.favicon then + site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">' + end + + local self = { + version = site_config.version, + title = site_config.title, + copyright = site_config.copyright, + favicon = site_config.favicon, + header = header or [[ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,initial-scale=1.0"> +{{FAVICON}} +<title>{{TITLE}}</title> +<style> +:root { + --bg: #f5f5f5; + --text: #111827; + --muted: #6b7280; + --link: #1a0dab; + --accent: #68a6ff; + --highlight: #004d99; + --note-bg: #ffffff; + --panel-bg: #ffffff; + --border: rgba(0,0,0,.1); + --table-head: #f3f4f6; + --code-color: #004d99; + --blockquote-border: #1a73e8; + --banner-bg: #ffffff; + --footer-bg: #ffffff; + --shadow: rgba(0,0,0,.08); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f1113; + --text: #e6eef3; + --muted: #9aa6b1; + --link: #68a6ff; + --accent: #68a6ff; + --highlight: #cde7ff; + --note-bg: #1a1c20; + --panel-bg: #1a1c20; + --border: rgba(255,255,255,.06); + --table-head: #1a1c20; + --code-color: #cde7ff; + --blockquote-border: #68a6ff; + --banner-bg: #1a1c20; + --footer-bg: #1a1c20; + --shadow: rgba(0,0,0,.4); + } +} + +html, body { + min-height: 100%; + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { padding: 36px; } + +.container { max-width: 830px; margin: 0 auto; } + +.container > *:not(.banner) { margin: 28px 0; } + +h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; } + +h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; } + +h2 { font-size: 32px; margin: 26px 0 14px; } + +h3 { font-size: 26px; margin: 22px 0 12px; } + +h4 { font-size: 20px; margin: 18px 0 10px; } + +h5 { font-size: 16px; margin: 16px 0 8px; } + +h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); } + +p { margin: 14px 0; } + +a { color: var(--link); text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; } + +.hidden { color: var(--text); text-decoration: none; } + +a:hover { text-decoration: underline; } + +summary { cursor: pointer; } + +details { + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 14px 16px; + margin: 16px 0; +} + +details summary { + list-style: none; + font-weight: 600; + color: var(--text); + display: flex; + align-items: center; +} + +details summary::-webkit-details-marker { display: none; } + +details summary::before { + content: "▸"; + margin-right: 8px; + transition: transform .15s ease; + color: var(--accent); +} + +details[open] summary::before { transform: rotate(90deg); } + +summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); } + +details[open] summary::after { content: "Collapse"; } + +details > *:not(summary) { margin-top: 12px; } + +.note, pre, code { + background: var(--note-bg); + border: 1px solid var(--border); +} + +.note { + padding: 20px; + border-radius: 4px; + background: var(--note-bg); + border: 1px solid var(--border); + margin: 28px 0; + color: var(--text); +} + +.note strong { color: var(--text); } + +.muted { color: var(--muted); } + +.lead { font-size: 15px; margin-top: 8px; } + +.callout { display: block; margin: 12px 0; } + +.small { font-size: 13px; color: var(--muted); margin-top: 6px; } + +.highlight { font-weight: 700; color: var(--highlight); } + +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: var(--code-color); +} + +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 var(--blockquote-border); + padding-left: 18px; + margin: 14px 0; + color: var(--text); + font-style: italic; +} + +hr { border: 0; border-top: 1px solid rgba(0,0,0,.08); margin: 26px 0; } + +@media (prefers-color-scheme: dark) { + hr { border-top-color: rgba(255,255,255,.1); } +} + +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 var(--border); +} + +th { + background: var(--table-head); + font-weight: 600; + color: var(--text); +} + +tr:hover { background: rgba(0,0,0,0.02); } + +@media (prefers-color-scheme: dark) { + tr:hover { background: rgba(255,255,255,0.02); } +} + +.divider { margin: 26px 0; height: 1px; background: rgba(0,0,0,.08); } + +@media (prefers-color-scheme: dark) { + .divider { background: rgba(255,255,255,.1); } +} + +.section { margin-top: 36px; } + +.links { margin: 12px 0; } + +.links a { display: inline-block; margin: 0 14px 6px 0; color: var(--link); } + +strong, b { font-weight: 600; color: var(--text); } + +em, i { font-style: italic; } + +.center { display: flex; justify-content: center; align-items: center; } + +.banner { + width: 100%; + box-sizing: border-box; + text-align: center; + background: var(--banner-bg); + padding: 20px; + border: 1px solid var(--border); + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + color: var(--text); + margin: -36px 0 28px 0; + box-shadow: 0 0.2em 0.6em var(--shadow); +} + +.nav { margin-left: auto; margin-right: auto; } + +.nav a { color: var(--highlight); } + +.footer { + background: var(--footer-bg); + padding: 20px 0; + border-top: 1px solid rgba(0,0,0,.08); + font-size: 14px; + color: var(--muted); + display: flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-top: 28px !important; + margin-bottom: 0; +} + +.left { text-align: left; float: left; } + +.right { text-align: right; float: right; } +</style> +</head> +<body> +<div class="container"> +]], + footer = footer or [[ +<footer class="footer"> +<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a> +<a href="https://www.lua.org/" target="_blank">Lua Powered</a> +<a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a> +<p>{{COPYRIGHT}}</p> +</footer> +</div> +</body> +</html> +]], + parts = {} + } + + return setmetatable(self, M) +end + +function M:custom(str) + table.insert(self.parts, str) + return self +end + +for name, func in pairs(std) do + if type(func) == "function" then + M[name] = function(self, ...) + local result = func(...) + table.insert(self.parts, result) + return self + end + end +end + +function M:build() + local header = self.header + header = header:gsub("{{TITLE}}", self.title or "Document") + local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">') + header = header:gsub("{{FAVICON}}", favicon_html or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]) + local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") + return header .. table.concat(self.parts, "\n") .. footer +end + +M.__tostring = function(self) + return self:build() +end + +return M diff --git a/core/std.lua b/lib/std.lua diff --git a/core/symbol.lua b/lib/symbol.lua diff --git a/lib/util.lua b/lib/util.lua @@ -0,0 +1,14 @@ +local std = require("lib.std") +local symbol = require("lib.symbol") + +local M = {} + +function M.cc(tbl) + return table.concat(tbl) +end + +function M.copyright(link, holder) + return symbol.copyright .. " " .. std.external(link, holder) +end + +return M diff --git a/main.go b/main.go @@ -9,15 +9,15 @@ import ( "github.com/fatih/color" - "fes/src/config" - "fes/src/doc" - "fes/src/new" - "fes/src/server" - "fes/src/version" + "fes/modules/config" + "fes/modules/doc" + "fes/modules/new" + "fes/modules/server" + "fes/modules/version" ) -//go:embed core/* -var core embed.FS +//go:embed lib/* +var lib embed.FS //go:embed index.html var documentation string @@ -25,7 +25,8 @@ var documentation string func init() { config.Port = flag.Int("p", 3000, "Set the server port") config.Color = flag.Bool("no-color", false, "Disable color output") - config.Core = core + config.Static = flag.Bool("static", false, "Render and save all pages.") + config.Lib = lib config.Doc = documentation } @@ -38,15 +39,22 @@ func main() { fmt.Println(" run <project_dir> Start the server") fmt.Println("Options:") flag.PrintDefaults() + fmt.Println("For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.") } showVersion := flag.Bool("version", false, "Show version and exit") + showFullVersion := flag.Bool("V1", false, "Show extended version information and exit") + flag.Parse() if *showVersion { version.Version() } + if *showFullVersion { + version.FullVersion() + } + if *config.Color { color.NoColor = true } diff --git a/modules/config/config.go b/modules/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "embed" + "errors" +) + +var Lib embed.FS +var Doc string +var Port *int +var Color *bool +var Static *bool + +type AppConfig struct { + App struct { + Name string `toml:"name"` + Version string `toml:"version"` + Authors []string `toml:"authors"` + } `toml:"app"` +} + +var ErrRouteMiss = errors.New("not found") diff --git a/modules/doc/doc.go b/modules/doc/doc.go @@ -0,0 +1,22 @@ +package doc + +import ( + "fes/modules/config" + "fmt" + "os" + "path/filepath" + + "github.com/pkg/browser" +) + +func Open() error { + fmt.Println("Opening documentation in browser") + + tmpFile := filepath.Join(os.TempDir(), "doc.html") + + if err := os.WriteFile(tmpFile, []byte(config.Doc), 0644); err != nil { + return err + } + + return browser.OpenFile(tmpFile) +} diff --git a/src/new/new.go b/modules/new/new.go diff --git a/modules/server/server.go b/modules/server/server.go @@ -0,0 +1,442 @@ +package server + +import ( + "fes/modules/config" + "fes/modules/ui" + "fmt" + "github.com/pelletier/go-toml/v2" + lua "github.com/yuin/gopher-lua" + "html/template" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" +) + +type reqData struct { + path string + params map[string]string +} + +func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { + for _, entry := range entries { + path := filepath.Join(dir, entry.Name()) + if entry.IsDir() { + nextBase := joinBase(base, entry.Name()) + subEntries, err := os.ReadDir(path) + if err != nil { + return fmt.Errorf("failed to read directory %s: %w", path, err) + } + if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil { + return err + } + continue + } + route := joinBase(base, entry.Name()) + if !isStatic && strings.HasSuffix(entry.Name(), ".lua") { + name := strings.TrimSuffix(entry.Name(), ".lua") + if name == "index" { + routes[basePath(base)] = path + routes[route] = path + continue + } + route = joinBase(base, name) + } else if !isStatic && strings.HasSuffix(entry.Name(), ".md") { + name := strings.TrimSuffix(entry.Name(), ".md") + if name == "index" { + routes[basePath(base)] = path + routes[route] = path + continue + } + route = joinBase(base, name) + } + routes[route] = path + } + return nil +} + +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() || !strings.HasSuffix(e.Name(), ".lua") { + continue + } + base := strings.TrimSuffix(e.Name(), ".lua") + path := filepath.Join(includeDir, e.Name()) + if _, err := os.Stat(path); err != nil { + tbl := L.NewTable() + tbl.RawSetString("error", lua.LString(fmt.Sprintf("file not found: %s", path))) + app.RawSetString(base, tbl) + continue + } + if err := L.DoFile(path); err != nil { + tbl := L.NewTable() + tbl.RawSetString("error", lua.LString(err.Error())) + app.RawSetString(base, tbl) + continue + } + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok || tbl == nil { + tbl = L.NewTable() + } + app.RawSetString(base, tbl) + } + return app +} + +func loadLua(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) { + L := lua.NewState() + defer L.Close() + + libFiles, err := fs.ReadDir(config.Lib, "lib") + if err == nil { + for _, de := range libFiles { + if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { + continue + } + path := filepath.Join("lib", de.Name()) + fileData, err := config.Lib.ReadFile(path) + if err != nil { + continue + } + L.DoString(string(fileData)) + } + } + + preloadLuaModule := func(name, path string) { + L.PreloadModule(name, func(L *lua.LState) int { + fileData, err := config.Lib.ReadFile(path) + if err != nil { + panic(err) + } + if err := L.DoString(string(fileData)); err != nil { + panic(err) + } + L.Push(L.Get(-1)) + return 1 + }) + } + + preloadLuaModule("lib.std", "lib/std.lua") + preloadLuaModule("lib.symbol", "lib/symbol.lua") + preloadLuaModule("lib.util", "lib/util.lua") + + L.PreloadModule("fes", func(L *lua.LState) int { + mod := L.NewTable() + libModules := []string{} + if ents, err := fs.ReadDir(config.Lib, "lib"); err == nil { + for _, e := range ents { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { + continue + } + libModules = append(libModules, strings.TrimSuffix(e.Name(), ".lua")) + } + } + for _, modName := range libModules { + path := filepath.Join("lib", modName+".lua") + fileData, err := config.Lib.ReadFile(path) + if err != nil { + continue + } + if err := L.DoString(string(fileData)); err != nil { + continue + } + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok || tbl == nil { + tbl = L.NewTable() + } + if modName == "fes" { + tbl.ForEach(func(k, v lua.LValue) { mod.RawSet(k, v) }) + } else { + mod.RawSetString(modName, tbl) + } + } + + mod.RawSetString("app", loadIncludeModules(L, filepath.Join(".", "include"))) + + if cfg != nil { + site := L.NewTable() + site.RawSetString("version", lua.LString(cfg.App.Version)) + site.RawSetString("name", lua.LString(cfg.App.Name)) + authors := L.NewTable() + for i, a := range cfg.App.Authors { + authors.RawSetInt(i+1, lua.LString(a)) + } + site.RawSetString("authors", authors) + mod.RawSetString("site", site) + } + + bus := L.NewTable() + bus.RawSetString("url", lua.LString(requestData.path)) + params := L.NewTable() + for k, v := range requestData.params { + params.RawSetString(k, lua.LString(v)) + } + bus.RawSetString("params", params) + mod.RawSetString("bus", bus) + + mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { + L.Push(lua.LString(markdownToHTML(L.ToString(1)))) + return 1 + })) + + L.Push(mod) + return 1 + }) + + if err := L.DoFile(entry); err != nil { + return []byte(""), err + } + + if L.GetTop() == 0 { + return []byte(""), nil + } + + L.SetGlobal("__fes_result", L.Get(-1)) + if err := L.DoString("return tostring(__fes_result)"); err != nil { + L.GetGlobal("__fes_result") + if s := L.ToString(-1); s != "" { + return []byte(s), nil + } + return []byte(""), nil + } + + if s := L.ToString(-1); s != "" { + return []byte(s), nil + } + return []byte(""), nil +} + +func generateArchiveIndex(fsPath string, urlPath string) (string, error) { + info, err := os.Stat(fsPath) + if err != nil { + return "", err + } + if !info.IsDir() { + return "", fmt.Errorf("not a directory") + } + ents, err := os.ReadDir(fsPath) + if err != nil { + return "", err + } + type entryInfo struct { + name string + isDir bool + href string + size int64 + mod time.Time + } + var list []entryInfo + for _, e := range ents { + n := e.Name() + full := filepath.Join(fsPath, n) + st, err := os.Stat(full) + if err != nil { + continue + } + isd := st.IsDir() + displayName := n + if isd { + displayName = n + "/" + } + href := path.Join(urlPath, n) + if isd && !strings.HasSuffix(href, "/") { + href = href + "/" + } + size := int64(-1) + if !isd { + size = st.Size() + } + list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()}) + } + sort.Slice(list, func(i, j int) bool { + if list[i].isDir != list[j].isDir { + return list[i].isDir + } + return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) + }) + + urlPath = basePath(strings.TrimPrefix(urlPath, "/archive")) + + var b strings.Builder + b.WriteString("<html>\n<head><title>Index of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("</title></head>\n<body>\n<h1>Index of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("</h1><hr><pre>") + if urlPath != "/archive" && urlPath != "/archive/" { + up := path.Dir(urlPath) + if up == "." { + up = "/archive" + } + if !strings.HasSuffix(up, "/") { + up = "/archive" + filepath.Dir(up) + "/" + } + b.WriteString(`<a href="` + template.HTMLEscapeString(up) + `">../</a>` + "\n") + } else { + b.WriteString(`<a href="../">../</a>` + "\n") + } + nameCol := 50 + for _, ei := range list { + escapedName := template.HTMLEscapeString(ei.name) + dateStr := ei.mod.Local().Format("02-Jan-2006 15:04") + var sizeStr string + if ei.isDir { + sizeStr = "-" + } else { + sizeStr = fmt.Sprintf("%d", ei.size) + } + spaces := 1 + if len(escapedName) < nameCol { + spaces = nameCol - len(escapedName) + } + line := `<a href="` + template.HTMLEscapeString(ei.href) + `">` + escapedName + `</a>` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n" + b.WriteString(line) + } + b.WriteString("</pre><hr></body>\n</html>") + return b.String(), nil +} + +func generateNotFoundData(cfg *config.AppConfig) []byte { + notFoundData := []byte(` +<html> +<head><title>404 Not Found</title></head> +<body> +<center><h1>404 Not Found</h1></center> +<hr><center>fes</center> +</body> +</html> +`) + if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil { + if nf, err := loadLua("www/404.lua", cfg, reqData{}); err == nil { + notFoundData = nf + } + } else if _, err := os.Stat("www/404.html"); err == nil { + if buf, err := os.ReadFile("www/404.html"); err == nil { + notFoundData = buf + } + } + return notFoundData +} + +func loadDirs() map[string]string { + routes := make(map[string]string) + + if entries, err := os.ReadDir("www"); err == nil { + if err := handleDir(entries, "www", routes, "", false); err != nil { + ui.Warning("failed to handle www directory", err) + } + } + + if entries, err := os.ReadDir("static"); err == nil { + if err := handleDir(entries, "static", routes, "/static", true); err != nil { + ui.Warning("failed to handle static directory", err) + } + } + + if entries, err := os.ReadDir("archive"); err == nil { + if err := handleDir(entries, "archive", routes, "/archive", true); err != nil { + ui.Warning("failed to handle archive directory", err) + } + } + + return routes +} + +func parseConfig() config.AppConfig { + tomlDocument, err := os.ReadFile("Fes.toml") + if err != nil { + ui.Error("failed to read Fes.toml", err) + os.Exit(1) + } + docStr := fixMalformedToml(string(tomlDocument)) + var cfg config.AppConfig + if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil { + ui.Warning("failed to parse Fes.toml", err) + cfg.App.Authors = []string{"unknown"} + cfg.App.Name = "unknown" + cfg.App.Version = "unknown" + } + return cfg +} + +func readArchive(w http.ResponseWriter, route string) { + fsPath := "." + route + if info, err := os.Stat(fsPath); err == nil && info.IsDir() { + if page, err := generateArchiveIndex(fsPath, route); err == nil { + w.Write([]byte(page)) + } + } +} + +func Start(dir string) error { + if err := os.Chdir(dir); err != nil { + return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) + } + + cfg := parseConfig() + notFoundData := generateNotFoundData(&cfg) + routes := loadDirs() + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + route, ok := routes[r.URL.Path] + + var err error = nil + + /* defer won't update paramaters unless we do this. */ + defer func() { + ui.Path(route, err) + }() + + if !ok { + err = config.ErrRouteMiss + route = r.URL.Path + + if strings.HasPrefix(route, "/archive") { + readArchive(w, route) + } else { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(notFoundData)) + } + return + } + + params := make(map[string]string) + for k, v := range r.URL.Query() { + if len(v) > 0 { + params[k] = v[0] + } + } + + var data []byte + if strings.HasSuffix(route, ".lua") { + data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params}) + } else if strings.HasSuffix(route, ".md") { + data, err = os.ReadFile(route) + data = []byte(markdownToHTML(string(data))) + } else { + data, err = os.ReadFile(route) + } + + if err != nil { + http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError) + } + + w.Write(data) + }) + + fmt.Printf("Server is running on http://localhost:%d\n", *config.Port) + return http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil) +} diff --git a/modules/server/util.go b/modules/server/util.go @@ -0,0 +1,45 @@ +package server + +import ( + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "regexp" + "strings" +) + +func joinBase(base, name string) string { + if base == "" { + return "/" + name + } + return base + "/" + name +} + +func basePath(base string) string { + if base == "" || base == "." { + return "/" + } + return base +} + +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)) +} diff --git a/modules/ui/ui.go b/modules/ui/ui.go @@ -0,0 +1,57 @@ +package ui + +import ( + "errors" + "fmt" + "strings" + + "fes/modules/config" + "fes/modules/version" + + "github.com/fatih/color" +) + +func Path(path string, err error) { + path = strings.TrimPrefix(path, "/") + + if path == "" { + path = "(null)" + } + + fmt.Printf(" > %s ", path) + if err == nil { + OK("ok") + return + } else if errors.Is(err, config.ErrRouteMiss) { + WARN(config.ErrRouteMiss.Error()) + } else { + ERROR("bad") + } +} + +func Warning(msg string, err error) error { + fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err) + return err +} + +func Error(msg string, err error) error { + fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err) + return err +} + +func Fatal(msg string, err error) error { + fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err) + panic(err) +} + +func OK(msg string) { + color.Green(msg) +} + +func WARN(msg string) { + color.Magenta(msg) +} + +func ERROR(msg string) { + color.Red(msg) +} diff --git a/modules/version/version.go b/modules/version/version.go @@ -0,0 +1,22 @@ +package version + +import ( + "fmt" + "os" +) + +var gitCommit string = "devel" + +const PROGRAM_NAME string = "fes" +const PROGRAM_NAME_LONG string = "fes/fSD" +const VERSION string = "beta" + +func Version() { + fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION) + os.Exit(0) +} + +func FullVersion() { + fmt.Printf("%s+%s\n", VERSION, gitCommit) + os.Exit(0) +} diff --git a/src/config/config.go b/src/config/config.go @@ -1,21 +0,0 @@ -package config - -import ( - "embed" - "errors" -) - -var Core embed.FS -var Doc string -var Port *int -var Color *bool - -type MyConfig struct { - App struct { - Name string `toml:"name"` - Version string `toml:"version"` - Authors []string `toml:"authors"` - } `toml:"app"` -} - -var ErrRouteMiss = errors.New("not found") diff --git a/src/doc/doc.go b/src/doc/doc.go @@ -1,22 +0,0 @@ -package doc - -import ( - "fes/src/config" - "fmt" - "os" - "path/filepath" - - "github.com/pkg/browser" -) - -func Open() error { - fmt.Println("Opening documentation in browser") - - tmpFile := filepath.Join(os.TempDir(), "doc.html") - - if err := os.WriteFile(tmpFile, []byte(config.Doc), 0644); err != nil { - return err - } - - return browser.OpenFile(tmpFile) -} diff --git a/src/server/server.go b/src/server/server.go @@ -1,484 +0,0 @@ -package server - -import ( - "fmt" - "html/template" - "io/fs" - "net/http" - "os" - "path" - "path/filepath" - "regexp" - "sort" - "strings" - "time" - - "fes/src/config" - "fes/src/ui" - - "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" -) - -type reqData struct { - path string - params map[string]string -} - -func joinBase(base, name string) string { - if base == "" { - return "/" + name - } - return base + "/" + name -} - -func basePath(base string) string { - if base == "" || base == "." { - return "/" - } - return base -} - -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 handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { - for _, entry := range entries { - path := filepath.Join(dir, entry.Name()) - if entry.IsDir() { - nextBase := joinBase(base, entry.Name()) - subEntries, err := os.ReadDir(path) - if err != nil { - return fmt.Errorf("failed to read directory %s: %w", path, err) - } - if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil { - return err - } - continue - } - route := joinBase(base, entry.Name()) - if !isStatic && strings.HasSuffix(entry.Name(), ".lua") { - name := strings.TrimSuffix(entry.Name(), ".lua") - if name == "index" { - routes[basePath(base)] = path - routes[route] = path - continue - } - route = joinBase(base, name) - } else if !isStatic && strings.HasSuffix(entry.Name(), ".md") { - name := strings.TrimSuffix(entry.Name(), ".md") - if name == "index" { - routes[basePath(base)] = path - routes[route] = path - continue - } - route = joinBase(base, name) - } - routes[route] = path - } - return nil -} - -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() || !strings.HasSuffix(e.Name(), ".lua") { - continue - } - base := strings.TrimSuffix(e.Name(), ".lua") - path := filepath.Join(includeDir, e.Name()) - if _, err := os.Stat(path); err != nil { - tbl := L.NewTable() - tbl.RawSetString("error", lua.LString(fmt.Sprintf("file not found: %s", path))) - app.RawSetString(base, tbl) - continue - } - if err := L.DoFile(path); err != nil { - tbl := L.NewTable() - tbl.RawSetString("error", lua.LString(err.Error())) - app.RawSetString(base, tbl) - continue - } - val := L.Get(-1) - L.Pop(1) - tbl, ok := val.(*lua.LTable) - if !ok || tbl == nil { - tbl = L.NewTable() - } - app.RawSetString(base, tbl) - } - return app -} - -func loadLua(entry string, cfg *config.MyConfig, requestData reqData) ([]byte, error) { - L := lua.NewState() - defer L.Close() - - coreFiles, err := fs.ReadDir(config.Core, "core") - if err == nil { - for _, de := range coreFiles { - if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { - continue - } - path := filepath.Join("core", de.Name()) - fileData, err := config.Core.ReadFile(path) - if err != nil { - continue - } - L.DoString(string(fileData)) - } - } - - preloadLuaModule := func(name, path string) { - L.PreloadModule(name, func(L *lua.LState) int { - fileData, err := config.Core.ReadFile(path) - if err != nil { - panic(err) - } - if err := L.DoString(string(fileData)); err != nil { - panic(err) - } - L.Push(L.Get(-1)) - return 1 - }) - } - - preloadLuaModule("core.std", "core/std.lua") - preloadLuaModule("core.symbol", "core/symbol.lua") - preloadLuaModule("core.util", "core/util.lua") - - 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() || !strings.HasSuffix(e.Name(), ".lua") { - continue - } - coreModules = append(coreModules, strings.TrimSuffix(e.Name(), ".lua")) - } - } - for _, modName := range coreModules { - path := filepath.Join("core", modName+".lua") - fileData, err := config.Core.ReadFile(path) - if err != nil { - continue - } - if err := L.DoString(string(fileData)); 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) - } - } - - mod.RawSetString("app", loadIncludeModules(L, filepath.Join(".", "include"))) - - if cfg != nil { - site := L.NewTable() - site.RawSetString("version", lua.LString(cfg.App.Version)) - site.RawSetString("name", lua.LString(cfg.App.Name)) - authors := L.NewTable() - for i, a := range cfg.App.Authors { - authors.RawSetInt(i+1, lua.LString(a)) - } - site.RawSetString("authors", authors) - mod.RawSetString("site", site) - } - - bus := L.NewTable() - bus.RawSetString("url", lua.LString(requestData.path)) - params := L.NewTable() - for k, v := range requestData.params { - params.RawSetString(k, lua.LString(v)) - } - bus.RawSetString("params", params) - mod.RawSetString("bus", bus) - - mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { - L.Push(lua.LString(markdownToHTML(L.ToString(1)))) - return 1 - })) - - L.Push(mod) - return 1 - }) - - if err := L.DoFile(entry); err != nil { - return []byte(""), err - } - - if L.GetTop() == 0 { - return []byte(""), nil - } - - L.SetGlobal("__fes_result", L.Get(-1)) - if err := L.DoString("return tostring(__fes_result)"); err != nil { - L.GetGlobal("__fes_result") - if s := L.ToString(-1); s != "" { - return []byte(s), nil - } - return []byte(""), nil - } - - if s := L.ToString(-1); s != "" { - return []byte(s), nil - } - return []byte(""), nil -} - -func generateArchiveIndex(fsPath string, urlPath string) (string, error) { - info, err := os.Stat(fsPath) - if err != nil { - return "", err - } - if !info.IsDir() { - return "", fmt.Errorf("not a directory") - } - ents, err := os.ReadDir(fsPath) - if err != nil { - return "", err - } - type entryInfo struct { - name string - isDir bool - href string - size int64 - mod time.Time - } - var list []entryInfo - for _, e := range ents { - n := e.Name() - full := filepath.Join(fsPath, n) - st, err := os.Stat(full) - if err != nil { - continue - } - isd := st.IsDir() - displayName := n - if isd { - displayName = n + "/" - } - href := path.Join(urlPath, n) - if isd && !strings.HasSuffix(href, "/") { - href = href + "/" - } - size := int64(-1) - if !isd { - size = st.Size() - } - list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()}) - } - sort.Slice(list, func(i, j int) bool { - if list[i].isDir != list[j].isDir { - return list[i].isDir - } - return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) - }) - - urlPath = basePath(strings.TrimPrefix(urlPath, "/archive")) - - var b strings.Builder - b.WriteString("<html>\n<head><title>Index of ") - b.WriteString(template.HTMLEscapeString(urlPath)) - b.WriteString("</title></head>\n<body>\n<h1>Index of ") - b.WriteString(template.HTMLEscapeString(urlPath)) - b.WriteString("</h1><hr><pre>") - if urlPath != "/archive" && urlPath != "/archive/" { - up := path.Dir(urlPath) - if up == "." { - up = "/archive" - } - if !strings.HasSuffix(up, "/") { - up = "/archive" + filepath.Dir(up) + "/" - } - b.WriteString(`<a href="` + template.HTMLEscapeString(up) + `">../</a>` + "\n") - } else { - b.WriteString(`<a href="../">../</a>` + "\n") - } - nameCol := 50 - for _, ei := range list { - escapedName := template.HTMLEscapeString(ei.name) - dateStr := ei.mod.Local().Format("02-Jan-2006 15:04") - var sizeStr string - if ei.isDir { - sizeStr = "-" - } else { - sizeStr = fmt.Sprintf("%d", ei.size) - } - spaces := 1 - if len(escapedName) < nameCol { - spaces = nameCol - len(escapedName) - } - line := `<a href="` + template.HTMLEscapeString(ei.href) + `">` + escapedName + `</a>` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n" - b.WriteString(line) - } - b.WriteString("</pre><hr></body>\n</html>") - return b.String(), nil -} - -func generateNotFoundData(cfg *config.MyConfig) []byte { - notFoundData := []byte(` -<html> -<head><title>404 Not Found</title></head> -<body> -<center><h1>404 Not Found</h1></center> -<hr><center>fes</center> -</body> -</html> -`) - if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil { - if nf, err := loadLua("www/404.lua", cfg, reqData{}); err == nil { - notFoundData = nf - } - } else if _, err := os.Stat("www/404.html"); err == nil { - if buf, err := os.ReadFile("www/404.html"); err == nil { - notFoundData = buf - } - } - return notFoundData -} - -func loadDirs() map[string]string { - routes := make(map[string]string) - - if entries, err := os.ReadDir("www"); err == nil { - if err := handleDir(entries, "www", routes, "", false); err != nil { - ui.Warning("failed to handle www directory", err) - } - } - - if entries, err := os.ReadDir("static"); err == nil { - if err := handleDir(entries, "static", routes, "/static", true); err != nil { - ui.Warning("failed to handle static directory", err) - } - } - - if entries, err := os.ReadDir("archive"); err == nil { - if err := handleDir(entries, "archive", routes, "/archive", true); err != nil { - ui.Warning("failed to handle archive directory", err) - } - } - - return routes -} - -func parseConfig() config.MyConfig { - tomlDocument, err := os.ReadFile("Fes.toml") - if err != nil { - ui.Error("failed to read Fes.toml", err) - os.Exit(1) - } - docStr := fixMalformedToml(string(tomlDocument)) - var cfg config.MyConfig - if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil { - ui.Warning("failed to parse Fes.toml", err) - cfg.App.Authors = []string{"unknown"} - cfg.App.Name = "unknown" - cfg.App.Version = "unknown" - } - return cfg -} - -func readArchive(w http.ResponseWriter, route string) { - fsPath := "." + route - if info, err := os.Stat(fsPath); err == nil && info.IsDir() { - if page, err := generateArchiveIndex(fsPath, route); err == nil { - w.Write([]byte(page)) - } - } -} - -func Start(dir string) error { - if err := os.Chdir(dir); err != nil { - return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) - } - - cfg := parseConfig() - notFoundData := generateNotFoundData(&cfg) - routes := loadDirs() - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - route, ok := routes[r.URL.Path] - - var err error = nil - - /* defer won't update paramaters unless we do this. */ - defer func() { - ui.Path(route, err) - }() - - if !ok { - err = config.ErrRouteMiss - route = r.URL.Path - - if strings.HasPrefix(route, "/archive") { - readArchive(w, route) - } else { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(notFoundData)) - } - return - } - - params := make(map[string]string) - for k, v := range r.URL.Query() { - if len(v) > 0 { - params[k] = v[0] - } - } - - var data []byte - if strings.HasSuffix(route, ".lua") { - data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params}) - } else if strings.HasSuffix(route, ".md") { - data, err = os.ReadFile(route) - data = []byte(markdownToHTML(string(data))) - } else { - data, err = os.ReadFile(route) - } - - if err != nil { - http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError) - } - - w.Write(data) - }) - - fmt.Printf("Server is running on http://localhost:%d\n", *config.Port) - return http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil) -} diff --git a/src/ui/ui.go b/src/ui/ui.go @@ -1,57 +0,0 @@ -package ui - -import ( - "errors" - "fmt" - "strings" - - "fes/src/config" - "fes/src/version" - - "github.com/fatih/color" -) - -func Path(path string, err error) { - path = strings.TrimPrefix(path, "/") - - if path == "" { - path = "(null)" - } - - fmt.Printf(" > %s ", path) - if err == nil { - OK("ok") - return - } else if errors.Is(err, config.ErrRouteMiss) { - WARN(config.ErrRouteMiss.Error()) - } else { - ERROR("bad") - } -} - -func Warning(msg string, err error) error { - fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err) - return err -} - -func Error(msg string, err error) error { - fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err) - return err -} - -func Fatal(msg string, err error) error { - fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err) - panic(err) -} - -func OK(msg string) { - color.Green(msg) -} - -func WARN(msg string) { - color.Magenta(msg) -} - -func ERROR(msg string) { - color.Red(msg) -} diff --git a/src/version/version.go b/src/version/version.go @@ -1,15 +0,0 @@ -package version - -import ( - "fmt" - "os" -) - -const PROGRAM_NAME string = "fes" -const PROGRAM_NAME_LONG string = "fes/fSD" -const VERSION string = "beta" - -func Version() { - fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION) - os.Exit(0) -}