commit e4eb7d62e40592e25ab3a236ea227346ba8bad05
parent 9d7dbc31ca2da16ffdd26d0f68d6934ed09861e6
Author: vx-clutch <[email protected]>
Date: Fri, 26 Dec 2025 10:06:23 -0500
large changes
Diffstat:
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 "© 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 "© 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)
-}