Compare commits

12 Commits

Author SHA1 Message Date
vx-clutch
7a6625fea7 First version of Gemini formatting 2026-03-01 21:51:37 -05:00
vx-clutch
6b87f3794b Update todos 2026-03-01 21:51:27 -05:00
vx-clutch
865e22deb7 Move TODO -> todo 2026-03-01 11:46:25 -05:00
1dd76cf1d2 save 2026-02-24 15:59:26 -05:00
ebd14a1b36 update build flags
update build flags
2026-02-16 21:48:56 -05:00
f8287de023 add note back into the standard library 2026-02-16 21:43:05 -05:00
472e27b1fa add gemini support 2026-02-16 21:00:04 -05:00
94818e25fe WPI 2026-02-14 15:50:05 -05:00
8bfe979093 extract out protocol 2026-02-14 08:26:56 -05:00
d340c55e8c Merge branch 'api-change' of https://git.vxserver.dev/fsd/fes into api-change 2026-02-13 21:41:20 -05:00
4bd920b09f update api to use "blocks" for future protocols 2026-02-13 21:41:02 -05:00
b2dfe99326 update api to use "block" for future protocol expantion 2026-02-13 21:40:08 -05:00
49 changed files with 983 additions and 862 deletions

View File

@@ -1,2 +0,0 @@
# build deps
brew "go@1.25"

View File

@@ -8,7 +8,7 @@ deps:
$(GO) mod download
build: deps
CGO_ENABLED=0 $(GO) build -ldflags "-X fes/modules/version.gitCommit=$(shell git rev-parse --short HEAD)" -o fes
CGO_ENABLED=0 $(GO) build -trimpath -ldflags "-X fes/modules/version.gitCommit=$(shell git rev-parse --short HEAD) -s -w -buildid=" -o fes
@echo "Fes is now built to ./fes"
lint:

3
go.mod
View File

@@ -5,7 +5,6 @@ go 1.25.4
require (
github.com/fatih/color v1.18.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/yuin/gopher-lua v1.1.1
)
@@ -13,5 +12,5 @@ require (
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/sys v0.32.0 // indirect
)

6
go.sum
View File

@@ -7,8 +7,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
@@ -16,5 +14,5 @@ github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

View File

@@ -4,7 +4,111 @@ local symbol = require("lib.symbol")
local M = {}
M.__index = M
local default_html_header = [[
<!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:#fff; --panel-bg:#fff; --border:rgba(0,0,0,.1); --table-head:#f3f4f6; --code-color:#004d99; --blockquote-border:#1a73e8; --banner-bg:#fff; --footer-bg:#fff; --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; 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">
]]
local default_html_footer = [[
<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/main/LICENSE" target="_blank">ISC Licensed</a>
<p>{{COPYRIGHT}}</p>
</footer>
</div>
</body>
</html>
]]
local default_gemini_header = [[
]]
local default_gemini_footer = [[
=> https://git.vxserver.dev/fSD/fes Fes Powered
=> https://www.lua.org Lua Powered
=> https://git.vxserver.dev/fSD/fes/src/branch/main/LICENSE ISC Licensed
{{COPYRIGHT}}
]]
function M.fes(header, footer)
local proto = std.proto
local config = {}
local site_config = {}
local fes_mod = package.loaded.fes
@@ -15,8 +119,22 @@ function M.fes(header, footer)
end
end
if site_config.favicon then
site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">'
if proto == "http" and site_config.favicon then
site_config.favicon =
'<link rel="icon" type="image/x-icon" href="'
.. site_config.favicon
.. '">'
end
local default_header
local default_footer
if proto == "http" then
default_header = default_html_header
default_footer = default_html_footer
elseif proto == "gemini" then
default_header = default_gemini_header
default_footer = default_gemini_footer
end
local self = {
@@ -24,348 +142,54 @@ function M.fes(header, footer)
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>
]],
header = header or default_header,
footer = footer or default_footer,
parts = {},
proto = proto,
}
return setmetatable(self, M)
end
function M:g(str)
table.insert(self.parts, str)
return self
end
function M:extend(name, tbl)
if type(name) ~= "string" then
error("First argument to extend must be a string (namespace name)")
end
if type(tbl) ~= "table" then
error("Second argument to extend must be a table of functions")
end
self[name] = {}
for k, v in pairs(tbl) do
if type(v) ~= "function" then
error("Extension values must be functions, got " .. type(v) .. " for key " .. k)
end
self[name][k] = function(...)
return v(self, ...)
end
end
function M:raw(str)
table.insert(self.parts, (str or "") .. "\n")
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)
table.insert(self.parts, func(...))
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 symbol.legal.copyright .. "The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
if self.proto == "http" then
local header = self.header:gsub("{{TITLE}}", self.title or "Document")
local favicon_html = self.favicon
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>">'
header = header:gsub("{{FAVICON}}", favicon_html)
local footer = self.footer:gsub(
"{{COPYRIGHT}}",
self.copyright or symbol.legal.copyright .. "The Copyright Holder"
)
return header .. table.concat(self.parts, "\n") .. footer
elseif self.proto == "gemini" then
local footer = self.footer:gsub(
"{{COPYRIGHT}}",
self.copyright or "(c) The Copyright Holder"
)
local header = self.header
return header .. table.concat(self.parts, "\n") .. footer
end
return table.concat(self.parts, "\n")
end
M.__tostring = function(self)

View File

@@ -1,195 +1,150 @@
local M = {}
function M.element(tag, attrs, content)
local out = { "<", tag }
M.proto = "http"
M.__fes_banner_set = false
if attrs then
for k, v in pairs(attrs) do
if v ~= false and v ~= nil then
if v == true then
out[#out + 1] = " " .. k
else
out[#out + 1] = " " .. k .. "=\"" .. tostring(v) .. "\""
end
local function isHttp()
-- return M.proto == "http"
return false
end
local function isGemini()
-- return M.proto == "gemini"
return true
end
M.p = function(s)
s = s or ""
if isHttp() then
return "<p>" .. s .. "</p>"
elseif isGemini() then
return s
end
end
M.h = function(level, s)
level = tonumber(level) or 1
if level < 1 then level = 1 end
if level > 6 then level = 6 end
s = s or ""
if isHttp() then
return "<h" .. level .. ">" .. s .. "</h" .. level .. ">"
elseif isGemini() then
return "\n" .. string.rep("#", level) .. " " .. s .. "\n"
end
end
M.codeblock = function(s)
s = s or ""
if isHttp() then
return "<pre><code>" .. s .. "</code></pre>"
elseif isGemini() then
return "```\n" .. s .. "\n```"
end
end
M.inline = function(s)
s = s or ""
if isHttp() then
return "<code>" .. s .. "</code>"
elseif isGemini() then
return "`" .. s .. "`"
end
end
M.link = function(url, text)
url = url or ""
text = text or url
if isHttp() then
return "<a href=\"" .. url .. "\">" .. text .. "</a>"
elseif isGemini() then
return "=> " .. url .. " " .. text
end
end
M.list = function(items, ordered)
items = items or {}
if isHttp() then
local tag = ordered and "ol" or "ul"
local out = "<" .. tag .. ">"
for _, v in ipairs(items) do
out = out .. "<li>" .. v .. "</li>"
end
out = out .. "</" .. tag .. ">"
return out
elseif isGemini() then
local out = {}
for i, v in ipairs(items) do
if ordered then
table.insert(out, i .. ". " .. v)
else
table.insert(out, "* " .. v)
end
end
return table.concat(out, "\n")
end
end
M.blockquote = function(s)
s = s or ""
if isHttp() then
return "<blockquote>" .. s .. "</blockquote>"
elseif isGemini() then
return "> " .. string.gsub(s, "\n", "\n> ")
end
end
M.rule = function()
if isHttp() then
return "<hr />"
elseif isGemini() then
return "---"
end
end
M.image = function(alt, src)
alt = alt or ""
src = src or ""
if isHttp() then
return "<img src=\"" .. src .. "\" alt=\"" .. alt .. "\" />"
elseif isGemini() then
return "=> " .. src .. " " .. alt
end
end
M.file = function(text, url)
text = text or ""
url = url or ""
if isHttp() then
return "<a href=\"" .. url .. "\" download>" .. text .. "</a>"
elseif isGemini() then
return "=> " .. url .. " " .. text
end
end
M.note = function(text)
text = text or ""
if isHttp() then
return "<div class=\"note\">" .. text .. "</div>"
elseif isGemini() then
return "\n" .. text .. "\n"
end
end
M.banner = function (text)
text = text or ""
if M.__fes_banner_set then
error("Page already contains header")
return ""
end
if content == nil then
out[#out + 1] = " />"
return table.concat(out)
M.__fes_banner_set = true
if isHttp() then
return "<div class=\"banner\">" .. text .. "</div>"
elseif isGemini() then
return text
end
out[#out + 1] = ">"
out[#out + 1] = tostring(content)
out[#out + 1] = "</"
out[#out + 1] = tag
out[#out + 1] = ">"
return table.concat(out)
end
function M.a(link, str)
link = link or "https://example.com"
str = str or link
return M.element("a", { href = link }, str)
end
function M.download(link, str, downloadName)
link = link or "."
str = str or link
return M.element("a", { href = link, download = downloadName }, str)
end
function M.ha(link, str)
link = link or "https://example.com"
str = str or link
return M.element("a", { href = link, class = "hidden" }, str)
end
function M.external(link, str)
return M.element("a", { href = link, target = "_blank" }, str)
end
function M.note(str)
return M.element("div", { class = "note" }, str)
end
function M.muted(str)
return M.element("div", { class = "muted" }, str)
end
function M.callout(str)
return M.element("div", { class = "callout" }, str)
end
function M.h1(str)
return M.element("h1", nil, str or "")
end
function M.h2(str)
return M.element("h2", nil, str or "")
end
function M.h3(str)
return M.element("h3", nil, str or "")
end
function M.h4(str)
return M.element("h4", nil, str or "")
end
function M.h5(str)
return M.element("h5", nil, str or "")
end
function M.h6(str)
return M.element("h6", nil, str or "")
end
function M.p(str)
return M.element("p", nil, str or "")
end
function M.pre(str)
return M.element("pre", nil, str or "")
end
function M.code(str)
return M.element("pre", nil, M.element("code", nil, str or ""))
end
function M.ul(items)
items = items or {}
local out = {}
for _, item in ipairs(items) do
out[#out + 1] = M.element("li", nil, item)
end
return M.element("ul", nil, table.concat(out))
end
function M.ol(items)
items = items or {}
local out = {}
for _, item in ipairs(items) do
out[#out + 1] = M.element("li", nil, item)
end
return M.element("ol", nil, table.concat(out))
end
function M.tl(items)
items = items or {}
local out = {}
for _, item in ipairs(items) do
out[#out + 1] = M.element("li", nil, item)
end
return M.element("ul", { class = "tl" }, table.concat(out))
end
function M.blockquote(str)
return M.element("blockquote", nil, str or "")
end
function M.hr()
return M.element("hr")
end
function M.img(src, alt)
return M.element("img", { src = src or "", alt = alt or "" })
end
function M.strong(str)
return M.element("strong", nil, str or "")
end
function M.em(str)
return M.element("em", nil, str or "")
end
function M.br()
return M.element("br")
end
function M.div(content, class)
return M.element("div", class and { class = class } or nil, content or "")
end
function M.span(content, class)
return M.element("span", class and { class = class } or nil, content or "")
end
function M.escape(str)
str = tostring(str or "")
str = str:gsub("&", "&amp;")
str = str:gsub("<", "&lt;")
str = str:gsub(">", "&gt;")
str = str:gsub('"', "&quot;")
str = str:gsub("'", "&#39;")
return str
end
function M.highlight(str)
return M.element("span", { class = "highlight" }, str or "")
end
function M.banner(str)
return M.element("div", { class = "banner" }, str or "")
end
function M.center(str)
return M.element("div", { class = "center" }, str or "")
end
function M.nav(link, str)
link = link or "example.com"
str = str or link
return M.element("a", { href = link, class = "nav" }, str)
end
function M.rl(r, l)
return
M.element("span", { class = "left" }, r or "") ..
M.element("span", { class = "right" }, l or "")
end
return M

View File

@@ -1,16 +1,9 @@
local std = require("lib.std")
local symbol = require("lib.symbol")
local M = {}
function M.cc(tbl, sep)
return table.concat(tbl, sep or "")
end
function M.copyright(link, holder)
return symbol.legal.copyright .. " " .. std.external(link, holder)
end
function M.ls(dir)
local p = io.popen('ls -A -1 -- ' .. string.format('%q', dir))
if not p then

View File

@@ -31,6 +31,7 @@ func init() {
config.Lib = lib
config.Doc = documentation
config.Verbose = flag.Bool("verbose", false, "Enable verbose logging")
config.Proto = flag.String("proto", "", "Force protocol")
}
func main() {
@@ -93,10 +94,10 @@ func main() {
os.Exit(1)
}
case "run":
ui.Log("Fes is starting")
if *config.Port == 3000 {
ui.WARNING("Using default port, this may lead to conflicts with other services")
}
ui.Log("Fes is starting")
ui.Log("Fes version=%s, commit=%s, just started", version.VERSION, version.GetCommit())
runtime.ReadMemStats(&m)

View File

@@ -12,6 +12,7 @@ var Color *bool
var Static *bool
var Docker *bool
var Verbose *bool
var Proto *string
type AppConfig struct {
App struct {

196
modules/gemini/gemini.go Normal file
View File

@@ -0,0 +1,196 @@
package gemini
import (
"bufio"
"crypto/tls"
"net"
"net/url"
"strings"
"sync"
)
const (
StatusSuccess = 20
StatusTemporaryFail = 40
StatusPermanentFail = 50
StatusNotFound = 51
StatusBadRequest = 59
)
type Handler interface {
ServeGemini(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
f(w, r)
}
type ServeMux struct {
mu sync.RWMutex
handlers map[string]Handler
}
func NewServeMux() *ServeMux {
return &ServeMux{
handlers: make(map[string]Handler),
}
}
func (m *ServeMux) Handle(pattern string, h Handler) {
m.mu.Lock()
m.handlers[pattern] = h
m.mu.Unlock()
}
func (m *ServeMux) HandleFunc(pattern string, f func(ResponseWriter, *Request)) {
m.Handle(pattern, HandlerFunc(f))
}
func (m *ServeMux) ServeGemini(w ResponseWriter, r *Request) {
m.mu.RLock()
defer m.mu.RUnlock()
path := r.URL.Path
for pattern, handler := range m.handlers {
if strings.HasPrefix(path, pattern) {
handler.ServeGemini(w, r)
return
}
}
w.WriteHeader(StatusNotFound, "not found")
}
var DefaultServeMux = NewServeMux()
func Handle(pattern string, h Handler) {
DefaultServeMux.Handle(pattern, h)
}
func HandleFunc(pattern string, f func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, f)
}
type Request struct {
URL *url.URL
Conn net.Conn
}
type ResponseWriter interface {
Write([]byte) (int, error)
WriteHeader(status int, meta string)
}
type response struct {
conn net.Conn
wroteHeader bool
}
func (r *response) WriteHeader(status int, meta string) {
if r.wroteHeader {
return
}
r.conn.Write([]byte(
strings.TrimSpace(
strings.Join([]string{
strings.TrimSpace(
strings.Join([]string{
string(rune(status/10 + '0')),
string(rune(status%10 + '0')),
}, ""),
),
meta,
}, " "),
) + "\r\n"))
r.wroteHeader = true
}
func (r *response) Write(b []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(StatusSuccess, "text/gemini")
}
return r.conn.Write(b)
}
type Server struct {
Addr string
Handler Handler
TLSConfig *tls.Config
}
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
handler := s.Handler
if handler == nil {
handler = DefaultServeMux
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
cfg := s.TLSConfig
if cfg == nil {
cfg = &tls.Config{}
}
cfg.Certificates = []tls.Certificate{cert}
ln, err := tls.Listen("tcp", s.Addr, cfg)
if err != nil {
return err
}
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go s.serve(conn, handler)
}
}
func (s *Server) serve(conn net.Conn, handler Handler) {
defer conn.Close()
reader := bufio.NewReader(conn)
line, err := reader.ReadString('\n')
if err != nil {
return
}
if len(line) > 1026 {
conn.Write([]byte("59 request too long\r\n"))
return
}
line = strings.TrimSpace(line)
u, err := url.Parse(line)
if err != nil {
conn.Write([]byte("59 bad request\r\n"))
return
}
req := &Request{
URL: u,
Conn: conn,
}
rw := &response{
conn: conn,
}
handler.ServeGemini(rw, req)
}
func ListenAndServeTLS(addr, certFile, keyFile string, h Handler) error {
s := &Server{
Addr: addr,
Handler: h,
}
return s.ListenAndServeTLS(certFile, keyFile)
}

View File

@@ -74,14 +74,9 @@ local site = fes.fes()
-- site.copyright = fes.util.copyright("https://example.com", "%s")
site:h1("Hello, World!")
site:h(1, "Hello, World!")
return site`, name)
write("Fes.toml", `[app]
name = "%s"
version = "0.0.1"
authors = ["%s"]`, dir, name)
write("README.md", strings.ReplaceAll(`# %s
$$$$$$
@@ -110,11 +105,112 @@ All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| $$fes run .$$ | Runs the project at $$.$$ |
| $$fes run .$$ | Runs the project at $$.$$ |
## Gemini
Fes supports many different protocols. One of which is the Gemini protocol; this
protocol has specific encryption standards which makes the providing $$key.pem$$
and a $$cert.pem$$ files. *Please* regenerate these files by running this command.
$$$$$$
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout key.pem \
-out cert.pem \
-subj "/C=US/ST=State/L=City/O=Local Development/OU=Localhost/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" \
-addext "basicConstraints=CA:FALSE" \
-addext "keyUsage=digitalSignature,keyEncipherment" \
-addext "extendedKeyUsage=serverAuth"
$$$$$$
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"), dir, dir)
write("key.pem", `-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCfl+m81U9+WLBn
PiJ1ZADnZC+BecXArguAdnbkgDlmfqe1eU6M0PBmXXFHEvJYOh2oYQZ7DMbuLLvo
DdjomXM4yb9Axu2KDHfflTcw3wHD3850ganf9rwVG460gkVfUGaRiokyCvEJKd05
BxKmx8Zh6G2tpetaDKm72ONYQFyaBCelYMzaBRpJ2kcPqk/gEhUSvQKVF4oDREm+
sTEkCTWsjUesiG0393t3psFa/SlZKsNXNrb+N6y9SElKVlQOT28r9YGiDfy5PiAQ
iFtQse2gqC4IhG2MGi0b4mu4Qa3+d2u3jsmn6Gqt92hKrbFOpz+Ci5GQ+zKXnE+j
/h40l1J3h/aTM5/qS7malAtXySMVzLQnJzai6IL+nBKbIOMCzP1Bns64Oo7YuuhV
Cx/sovrvgFF+8j4nLXOBgrx8llYB5cvgVYkNmccXtPzd7hZI6PHgZRGOd0uBKlZ+
OHeKhgdnz/a+pCUBwJe8JAzX7fzoZ+HLy26ADx2Uywh0Rjfj4gVOSZXaGS0ne0p5
PciN0xsWQdaAWPcVe6+YYRKS6l6qaWuGaPwVsZSvNgIMMPOgUn7+s2Bxh51DhqZT
4MDFUJWXFmTnyhHg3Cq7pWJR7pv2erv5q32b8iah5o+CP4W3FeZX1CcBuiIsrimh
vZZIa5W2vzpkLHbWgI2XGbx8yKRXowIDAQABAoICAENZb5lyB5cRRHh9XztdFYiQ
3f9s7UhP6qiu1aO+fPrFDm9mHwEMF7eLTYep9j3HYMazE3IQRU8z76SRW21lfJuF
gEGM8aeldV0UcnMcWXlY1J6ULaVHUb4yn/mLVE1R98cJyLYmqeutEB/F3VgmzJB6
7vYuI/EfkO2mLOMMXkfc4wJGpIyJRLvP8tcoj4bG+r+qphFXGrYgNmLUEiHcBRup
j4q/FCBfP2qSI90LI0zu3/rJK1aDFlHW1J8baWOUoBzUAX3rGzDth8iSUr7uJ5L6
Blsvz68lSM4QslbS2OOfcATJrE5Apex+kTOas023BPVJgwfFCmey3mUdk4+sIG+I
YpvrfUHBAbhRt1itGjjx/BH3HVtkwKsm3nNNgE8qS0qmziGVa60amLng0OgD4SOH
QKWrotcojYlWHywdHNAmiaWP+ZV9U0u4T59dkHhjqBoPuY64kNddRCLYItszqOmC
tamnflcoFPThdBnDpnXhLX+MnvQKIO2K2QTKjdYrkSMvEC/m96H49Vf4w+Fe3coc
SLsyMqHmYSG3+QtFN3JSmqDdBLQ+bFK0KZOk3IE2UK+lvabmtHuGLkYguur/fOnG
FvxcC+MIHHFrtSfqfvIrXeGFqe2Fb6DIKdMJFPV8iP5Px91A0GAGeabyktyya1n5
kk5nMJY3A7rTmnDO2PUBAoIBAQDSd+pvNGDf/We74+NODKQBcleCfF0NNzvRIuYH
9vel5jhyD28Y30XQS7+YBo3Y5Yo5pVlJ425Z5CveZOmr4VImuYG8LTbbGYY8ns3c
R/IdP1ByAcIa1eRGQNcKHid7RW4aEoFdvW3jZNj6/ukbJwd9ZHOt4HEUv5MOObn7
VXiXiwHjVy3X4uD5j0bHS4owd7S3Ygg0OneUzX3XJ+8qUgfU9B+lD3SbtsI4baoU
qUKcQc/0yXKvJvoz9oX2tjQOHqGrGwHdootc2CLnr0PMI7fKBwT3k63B+24xEGcx
OAPc5tUuiqIewiCn1oZbeCxmNnnimkAjDfG2KXMXj1R1G96BAoIBAQDCHnTNQhXH
tcfwnxj86E+Zd7ORS7iH/5eWvnspt2pnNCpufLFQlbdiL3d9ysSN9nw3+y15/o83
R79s7fLdC0+blc6q/ZQEdB1C+8YQa0nTlqqlZWVbRPjQUV+SfGssaZFEO4W7QWh3
hJL14HbWUFC3XSdMAeKTNOcgvfVvQ9M8HlJI1ZPo4Pe4rGNFIEsKUz/6HVE7HFgQ
OwcLSxZlSQSQ6/YErP/XJoyQUPjHldAjphV/rn28cvh2quoI35fbBuZu5pyKLXTg
RKbdWbeoIZTthRojpqnfqnIR+rPXIIiaMYhStdPshNUxLL7DX+/3yI05qq8R+yqa
2gLb2mJltuwjAoIBAF0WQI/ywK4Q7CKEBnLs0FT7d4z06EsCFOjI4KjBKIMtseVw
whhkGAKqnhDlRTObQmmAol81wgbsDiMMyvUEcUtDXQgXj12UinShYDd/cqxQ5omm
EW3BEHeqEfIdqCSzbqEFckY9lC6w2e8Zc4xY1M028psC28DrgmUWTxXEldOg3bLp
ShNj+1Eld46J8JLDPyCksTA4c89Sm8ffl75GDcS4PI7KqS59xKUki8cbnaRyz0Fb
H+gr+xmkfVfC+n8MOUDubwLR84Wa6sVCFWBio9UtCZteq8lSJUh6EsoIFl1LkxpE
orOr9LmG/mHSYwDKM1pwEtHuRuvkpUzUTeyF6QECggEAbm/vWZtgUsdbocyR5cix
CImuUlo2+MBz2KIz5c7grShjf4pXQpZ6x1Rj8d/7JRz3HM482Cv4BKZABNP3GMTH
nKeE9YjgvgvlXedpjovLa6JLIV/nYx6BQ9sXuXopaxIAQEZw1dDngx+ckGAMm+8D
jN5lbfugkMlHOTx5Nrzqn0hM3f0McjATHzCMJZayuoQUYNJvFWcRvuImJsmoSyVY
gK6Nv6lAwIHA9JXsg3f6+10Q3BxEkoMCUlj4XuX+OfDaBnwS0RX9aV4FZOcW8oNw
fBT+gwvdl08cKJht2lU7AiZt/UhO8j+8HobrXLHnDxw9JHKzuVIgsgqYF8ZNtrpz
6wKCAQEAqJc9xfNYL2jyJTVBvqX34Uct9xV1fq7z45B3gpPHj4tc0dBOIwPKSsu8
rGyefPuMDvbecJoaXqRZGQEcv03ecLVtHHDCsJcWavth9sCQJW9mnOxjOnLHdGnl
h2yIV4iVzNEx5XA1+0JRAAwFMXrC9LUZDTT61opjohMJSEySo/HVekoWG9A0bQGn
Ui+JbtLeuuCwwAGSgdAhVkjr27ANyFtjR63KVrDvSLbYFyZXKxrRYnZWs92Ho/g6
oTLnUSmkbLlhsUDjknAaNfp+enETSoPHcpkd7MeqVw2oYrLw6MCo4h2tBsIgtZEd
h6CgX4Gc3+bhvKx36XYxVwScPJUWsQ==
-----END PRIVATE KEY-----`)
write("cert.pem", `-----BEGIN CERTIFICATE-----
MIIFmzCCA4OgAwIBAgIUZGaMDLeCAQplfhhjXb2HaL6xpFYwDQYJKoZIhvcNAQEL
BQAwXTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MQwwCgYDVQQKDANPcmcxDTALBgNVBAsMBFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9z
dDAeFw0yNjAyMjQwMjUwMzVaFw0yNzAyMjQwMjUwMzVaMF0xCzAJBgNVBAYTAlVT
MQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UECgwDT3JnMQ0w
CwYDVQQLDARVbml0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCfl+m81U9+WLBnPiJ1ZADnZC+BecXArguAdnbkgDlm
fqe1eU6M0PBmXXFHEvJYOh2oYQZ7DMbuLLvoDdjomXM4yb9Axu2KDHfflTcw3wHD
3850ganf9rwVG460gkVfUGaRiokyCvEJKd05BxKmx8Zh6G2tpetaDKm72ONYQFya
BCelYMzaBRpJ2kcPqk/gEhUSvQKVF4oDREm+sTEkCTWsjUesiG0393t3psFa/SlZ
KsNXNrb+N6y9SElKVlQOT28r9YGiDfy5PiAQiFtQse2gqC4IhG2MGi0b4mu4Qa3+
d2u3jsmn6Gqt92hKrbFOpz+Ci5GQ+zKXnE+j/h40l1J3h/aTM5/qS7malAtXySMV
zLQnJzai6IL+nBKbIOMCzP1Bns64Oo7YuuhVCx/sovrvgFF+8j4nLXOBgrx8llYB
5cvgVYkNmccXtPzd7hZI6PHgZRGOd0uBKlZ+OHeKhgdnz/a+pCUBwJe8JAzX7fzo
Z+HLy26ADx2Uywh0Rjfj4gVOSZXaGS0ne0p5PciN0xsWQdaAWPcVe6+YYRKS6l6q
aWuGaPwVsZSvNgIMMPOgUn7+s2Bxh51DhqZT4MDFUJWXFmTnyhHg3Cq7pWJR7pv2
erv5q32b8iah5o+CP4W3FeZX1CcBuiIsrimhvZZIa5W2vzpkLHbWgI2XGbx8yKRX
owIDAQABo1MwUTAdBgNVHQ4EFgQUcyq+ZZWmAUejOPEQpHpl4hOllHswHwYDVR0j
BBgwFoAUcyq+ZZWmAUejOPEQpHpl4hOllHswDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEANwdjxlPHqvOOyNTFJcFhcNff3862/74CyCiglrstk/ea
txjyHxXFwXOtyAtm0tqaQPpzHWFM5MDqVD7Qip4aZrPcRvvfJ8zxXuakrgy7oI6i
Wl4/BXzvIoxh8MyFVC7VdGmuv11fq091RaNPlnz5lH9Qxhb6QCGbk73PjTXxD/7A
yusNiXuJvx3oRhLLGuksotFKEnngOq21Jla3ZPUmJTLAZdk/joXITk5Q64ZBp5uD
rpxFZDtdHTBuL3nmNPUHixsxQRFzCSloufRlatdI2ldDc+d2WLLFhkpdM35Oedf3
Hib0VXPgJ8j/w6RK+oYKMP27RojMFXALolzRBSQu3Zbd1bD1qVhh/dO5Uyn4E3mb
2LW5+a9zuabD9wt8smSU9V5ZYw5hcR0ANN7xYmaMvGFKrNPNkI3m0c2dyj22vgg0
zQqeMze4KxZqDf/gFifJ4UIybBAgc/p1uOIceFTSYplVXJQIgFACn9l6c4Zo0GZD
H48iKQLwlxDdUSM8aHtItWxRddLCL7lYNKtu2/qu8I9NKkjx4/Kn3I9Is/f37EIn
rF3ygUg3ee5zo3fvMjlhCNs6djm+ANx2KzdZkS7UU/jb0o+OSEvoMIgZh5NNgUlC
aVR3opS3PTqwu4plN0Evh5kr3nEOx0vcIK726DwnpgNRhcG9cNcSi2UB4NYCWaw=
-----END CERTIFICATE-----`)
ui.Hint("you can run this with `fes run %s`", dir)

View File

@@ -1,8 +1,9 @@
package server
import (
"errors"
"fmt"
"net/http"
"io"
"os"
"path"
"path/filepath"
@@ -95,15 +96,22 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
}
/* helper to read the archive files */
func readArchive(w http.ResponseWriter, route string) error {
fsPath := "." + route
if info, err := os.Stat(fsPath); err == nil && info.IsDir() {
if page, err := generateArchiveIndex(fsPath, route); err == nil {
w.Write([]byte(page))
return nil
} else {
return err
func readArchive(w io.Writer, route string, protcol Protocols) error {
switch protcol {
case HTTP:
fsPath := "." + route
if info, err := os.Stat(fsPath); err == nil && info.IsDir() {
if page, err := generateArchiveIndex(fsPath, route); err == nil {
w.Write([]byte(page))
return nil
} else {
return err
}
}
case GEMINI:
panic(errors.New("TODO"))
default:
panic(errors.New("Invalid Protocol"))
}
return nil
}

View File

@@ -59,18 +59,21 @@ func loadDirs() map[string]string {
routes := make(map[string]string)
if entries, err := os.ReadDir("www"); err == nil {
ui.LogVerbose("reading www/")
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 {
ui.LogVerbose("reading static/")
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 {
ui.LogVerbose("reading archive/")
if err := handleDir(entries, "archive", routes, "/archive", true); err != nil {
ui.Warning("failed to handle archive directory", err)
}

59
modules/server/gemini.go Normal file
View File

@@ -0,0 +1,59 @@
package server
import (
"fes/modules/config"
"fes/modules/gemini"
"fes/modules/ui"
"os"
"strings"
)
func geminiHandler(w gemini.ResponseWriter, r *gemini.Request) {
ui.LogVerbose("Received %s", r.URL.Path)
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") {
err = readArchive(w, route, GEMINI)
} else {
w.WriteHeader(gemini.StatusNotFound, "StatusNotFound")
}
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 = render(route, reqData{path: r.URL.Path, params: params}, GEMINI)
} else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data)))
data = []byte("<style>body {max-width: 80ch;}</style>\n" + string(data))
} else {
ui.LogVerbose("serving unrecognized file")
data, err = os.ReadFile(route)
}
if err != nil {
w.WriteHeader(-1, err.Error())
}
w.Write(data)
}

67
modules/server/http.go Normal file
View File

@@ -0,0 +1,67 @@
package server
import (
"fes/modules/config"
"fes/modules/ui"
"fmt"
"net/http"
"os"
"strings"
)
func httpHandler(w http.ResponseWriter, r *http.Request) {
ui.LogVerbose("Received %s", r.URL.Path)
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") {
err = readArchive(w, route, HTTP)
} else {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>fes</center>
</body>
</html>`))
}
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 = render(route, reqData{path: r.URL.Path, params: params}, HTTP)
} else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data)))
data = []byte("<style>body {max-width: 80ch;}</style>\n" + string(data))
} else {
ui.LogVerbose("serving unrecognized file")
data, err = os.ReadFile(route)
}
if err != nil {
http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError)
}
w.Write(data)
}

View File

@@ -3,21 +3,18 @@ package server
import (
"fes/modules/config"
"io/fs"
"os"
"path/filepath"
"strings"
lua "github.com/yuin/gopher-lua"
)
/* this is the request data we pass over the bus to the application, via the fes.bus interface */
type reqData struct {
path string
params map[string]string
}
/* returns a string of rendered html */
func render(luapath string, requestData reqData) ([]byte, error) {
func render(luapath string, requestData reqData, protocol Protocols) ([]byte, error) {
L := lua.NewState()
defer L.Close()
@@ -27,10 +24,8 @@ func render(luapath string, requestData reqData) ([]byte, error) {
continue
}
path := filepath.Join("lib", de.Name())
if data, err := config.Lib.ReadFile(path); err != nil {
continue
} else {
L.DoString(string(data))
if data, err := config.Lib.ReadFile(path); err == nil {
_ = L.DoString(string(data))
}
}
}
@@ -41,10 +36,11 @@ func render(luapath string, requestData reqData) ([]byte, error) {
if err != nil {
panic(err)
}
if err := L.DoString(string(fileData)); err != nil {
panic(err)
}
L.Push(L.Get(-1))
return 1
})
}
@@ -54,88 +50,71 @@ func render(luapath string, requestData reqData) ([]byte, error) {
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)
}
fileData, err := config.Lib.ReadFile("lib/fes.lua")
if err != nil {
panic(err)
}
mod.RawSetString("app", func() *lua.LTable {
app := L.NewTable()
includeDir := "include"
if err := L.DoString(string(fileData)); err != nil {
panic(err)
}
includes, err := os.ReadDir(includeDir)
if err != nil {
return app // load no includes
mod := L.Get(-1)
L.Pop(1)
tbl, ok := mod.(*lua.LTable)
if !ok {
panic("fes module did not return table")
}
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal("require"),
NRet: 1,
Protect: true,
}, lua.LString("lib.std")); err != nil {
panic(err)
}
stdMod := L.Get(-1)
L.Pop(1)
stdTbl, ok := stdMod.(*lua.LTable)
if !ok {
panic("lib.std did not return table")
}
proto := func() string {
switch protocol {
case HTTP:
return "http"
case GEMINI:
return "gemini"
default:
return "http"
}
}()
proto = "gemini"
for _, de := range includes {
if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") {
continue
}
base := strings.TrimSuffix(de.Name(), ".lua")
path := filepath.Join(includeDir, de.Name())
if _, err := os.Stat(path); err != nil {
continue
} else if err := L.DoFile(path); err != nil {
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
}())
stdTbl.RawSetString("proto", lua.LString(proto))
tbl.RawSetString("std", stdTbl)
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 {
tbl.RawSetString("bus", bus)
tbl.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int {
L.Push(lua.LString(markdownToHTML(L.ToString(1))))
return 1
}))
L.Push(mod)
L.Push(tbl)
return 1
})
@@ -147,16 +126,25 @@ func render(luapath string, requestData reqData) ([]byte, error) {
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
}
val := L.Get(-1)
L.Pop(1)
if val == lua.LNil {
return []byte(""), nil
}
if s := L.ToString(-1); s != "" {
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal("tostring"),
NRet: 1,
Protect: true,
}, val); err != nil {
return nil, err
}
s := L.ToString(-1)
L.Pop(1)
if s != "" {
return []byte(s), nil
}

View File

@@ -2,16 +2,23 @@ package server
import (
"fes/modules/config"
"fes/modules/gemini"
"fes/modules/ui"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
)
var routes map[string]string
type Protocols int
const (
HTTP Protocols = iota
GEMINI
)
var Routes map[string]string
func Start(dir string) {
if err := os.Chdir(dir); err != nil {
@@ -30,62 +37,41 @@ func Start(dir string) {
ui.Log("running root=%s, port=%d.", root, *config.Port)
routes := loadDirs()
ui.LogVerbose("start loading directories")
Routes = loadDirs()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
route, ok := routes[r.URL.Path]
http.HandleFunc("/", httpHandler)
gemini.HandleFunc("/", geminiHandler)
var err error = nil
var wg sync.WaitGroup
errs := make(chan error, 2)
/* defer won't update paramaters unless we do this. */
defer func() {
ui.Path(route, err)
}()
wg.Add(2)
if !ok {
err = config.ErrRouteMiss
route = r.URL.Path
go func() {
defer wg.Done()
errs <- http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)
}()
if strings.HasPrefix(route, "/archive") {
err = readArchive(w, route)
} else {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>fes</center>
</body>
</html>`))
}
return
}
go func() {
defer wg.Done()
errs <- gemini.ListenAndServeTLS(fmt.Sprintf("0.0.0.0:%d", *config.Port-1035), "cert.pem", "key.pem", nil)
}()
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 = render(route, reqData{path: r.URL.Path, params: params})
} else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data)))
data = []byte("<style>body {max-width: 80ch;}</style>\n" + 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)
})
ui.Log("Server initialized")
wg.Wait()
close(errs)
log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil))
var collected []error
for err := range errs {
if err != nil {
collected = append(collected, err)
}
}
if len(collected) > 0 {
fmt.Printf("errors: %v\n", collected)
return
}
}

View File

@@ -9,7 +9,7 @@ var gitCommit string = "devel"
const PROGRAM_NAME string = "fes"
const PROGRAM_NAME_LONG string = "fes/fSD"
const VERSION string = "1.2.0"
const VERSION string = "2.0.0"
func Version() {
fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION)

View File

@@ -1,7 +1,7 @@
# default
# test
```
fes new default
fes new test
```
> **Know what you are doing?** Delete this file. Have fun!

View File

@@ -1,5 +0,0 @@
[app]
name = "archive"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,4 +0,0 @@
# archive
This example demonstrates the archive feature of Fes it is useful for file
sharing purposes.

View File

@@ -1,22 +0,0 @@
Pinnipeds [2] are the seals and their relatives, a group of semi-aquatic marine
mammals. The Pinnipedia is in the Order Carnivora. There are three seal
families: Odobenidae (walruses), Otariidae (eared seals, including sea lions
and fur seals), and Phocidae (true seals).[3]
Seals are sleek-bodied and barrel-shaped. Their bodies are well adapted to the
aquatic habitat where they spend most of their lives. Pinnipeds have flippers
for hands, big bulky bodies, doggish faces, and big eyes. Unlike cetaceans,
pinnipeds have their noses on their faces, and each nostril of the nose closes
when the pinniped goes underwater. Like cetaceans, pinnipeds have a thick layer
of blubber (fat) just under their skin: this blubber keeps them warm in cold
waters and keeps them fed during times when food is not easily found. When they
cannot find food, they live off the fat in the blubber.
Pinnipeds are carnivorous. This means they eat only meat (such as fish or
squid) and not plants. However, almost all pinnipeds can be eaten by polar
bears, sharks and killer whales.
Seals are often trained in zoos or aquariums to put on shows. However, in
Sweden, it is illegal to train a seal to balance a ball on its nose.[4]
From [Pinniped Wikipedia](https://simple.wikipedia.org/wiki/Pinniped)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,10 +0,0 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!")
site:a("/archive", fes.std.h2("To the file room!"))
return site

View File

@@ -1,5 +0,0 @@
[app]
name = "hello"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,4 +0,0 @@
# hello
This is a very simple hello world program, the only difference between this and
default is this README.

View File

@@ -1,8 +0,0 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!")
return site

32
test/cert.pem Normal file
View File

@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFmzCCA4OgAwIBAgIUZGaMDLeCAQplfhhjXb2HaL6xpFYwDQYJKoZIhvcNAQEL
BQAwXTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MQwwCgYDVQQKDANPcmcxDTALBgNVBAsMBFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9z
dDAeFw0yNjAyMjQwMjUwMzVaFw0yNzAyMjQwMjUwMzVaMF0xCzAJBgNVBAYTAlVT
MQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEMMAoGA1UECgwDT3JnMQ0w
CwYDVQQLDARVbml0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCfl+m81U9+WLBnPiJ1ZADnZC+BecXArguAdnbkgDlm
fqe1eU6M0PBmXXFHEvJYOh2oYQZ7DMbuLLvoDdjomXM4yb9Axu2KDHfflTcw3wHD
3850ganf9rwVG460gkVfUGaRiokyCvEJKd05BxKmx8Zh6G2tpetaDKm72ONYQFya
BCelYMzaBRpJ2kcPqk/gEhUSvQKVF4oDREm+sTEkCTWsjUesiG0393t3psFa/SlZ
KsNXNrb+N6y9SElKVlQOT28r9YGiDfy5PiAQiFtQse2gqC4IhG2MGi0b4mu4Qa3+
d2u3jsmn6Gqt92hKrbFOpz+Ci5GQ+zKXnE+j/h40l1J3h/aTM5/qS7malAtXySMV
zLQnJzai6IL+nBKbIOMCzP1Bns64Oo7YuuhVCx/sovrvgFF+8j4nLXOBgrx8llYB
5cvgVYkNmccXtPzd7hZI6PHgZRGOd0uBKlZ+OHeKhgdnz/a+pCUBwJe8JAzX7fzo
Z+HLy26ADx2Uywh0Rjfj4gVOSZXaGS0ne0p5PciN0xsWQdaAWPcVe6+YYRKS6l6q
aWuGaPwVsZSvNgIMMPOgUn7+s2Bxh51DhqZT4MDFUJWXFmTnyhHg3Cq7pWJR7pv2
erv5q32b8iah5o+CP4W3FeZX1CcBuiIsrimhvZZIa5W2vzpkLHbWgI2XGbx8yKRX
owIDAQABo1MwUTAdBgNVHQ4EFgQUcyq+ZZWmAUejOPEQpHpl4hOllHswHwYDVR0j
BBgwFoAUcyq+ZZWmAUejOPEQpHpl4hOllHswDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEANwdjxlPHqvOOyNTFJcFhcNff3862/74CyCiglrstk/ea
txjyHxXFwXOtyAtm0tqaQPpzHWFM5MDqVD7Qip4aZrPcRvvfJ8zxXuakrgy7oI6i
Wl4/BXzvIoxh8MyFVC7VdGmuv11fq091RaNPlnz5lH9Qxhb6QCGbk73PjTXxD/7A
yusNiXuJvx3oRhLLGuksotFKEnngOq21Jla3ZPUmJTLAZdk/joXITk5Q64ZBp5uD
rpxFZDtdHTBuL3nmNPUHixsxQRFzCSloufRlatdI2ldDc+d2WLLFhkpdM35Oedf3
Hib0VXPgJ8j/w6RK+oYKMP27RojMFXALolzRBSQu3Zbd1bD1qVhh/dO5Uyn4E3mb
2LW5+a9zuabD9wt8smSU9V5ZYw5hcR0ANN7xYmaMvGFKrNPNkI3m0c2dyj22vgg0
zQqeMze4KxZqDf/gFifJ4UIybBAgc/p1uOIceFTSYplVXJQIgFACn9l6c4Zo0GZD
H48iKQLwlxDdUSM8aHtItWxRddLCL7lYNKtu2/qu8I9NKkjx4/Kn3I9Is/f37EIn
rF3ygUg3ee5zo3fvMjlhCNs6djm+ANx2KzdZkS7UU/jb0o+OSEvoMIgZh5NNgUlC
aVR3opS3PTqwu4plN0Evh5kr3nEOx0vcIK726DwnpgNRhcG9cNcSi2UB4NYCWaw=
-----END CERTIFICATE-----

View File

@@ -1,5 +0,0 @@
[app]
name = "best"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,23 +0,0 @@
# best
This is an example of best practices for the Fes framework.
## Parts
With best practice we can break our sites into a few parts.
## Index
The main page of the site loads in the header and the footer, as well as shows
some core information
## Include
Within include the header and footer are defined.
* **Header:** Site navigation and name display
* **Footer:** Extra and external information.
## Static
This is where we store our favicon.

View File

@@ -1,13 +0,0 @@
local footer = {}
footer.render = function(std)
return table.concat({
std.h2("Other resources"),
std.tl({
std.external("https://git.vxserver.dev/fSD/fes", "Fes source"),
std.external("https://docs.vxserver.dev/static/fes.html", "Documentation"),
}),
})
end
return footer

View File

@@ -1,7 +0,0 @@
local header = {}
header.render = function(std)
return std.center(std.ha("/", std.h1("Best Practices")))
end
return header

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,20 +0,0 @@
local fes = require("fes")
local std = fes.std
local u = fes.util
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site.title = "Best practices"
site.favicon = "/static/favicon.ico"
site:banner(fes.app.header.render(std))
site:note(u.cc {
std.h2("Hello, World!"),
std.p("This is an example of the best practices/canonical Fes site.")
})
site:note(fes.app.footer.render(std))
return site

View File

@@ -1,5 +0,0 @@
[app]
name = "default"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,8 +0,0 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!")
return site

View File

@@ -1,5 +0,0 @@
[app]
name = "error"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,6 +0,0 @@
# error
This shows what a Lua error looks like to the user. Lua errors are the most
common and the most critical so that is why they are shown to the user. Other,
lesser errors, are only shown to the developer because of their different
nature.

View File

@@ -1,10 +0,0 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
This is what an error looks like
site:h1("Hello, World!")
return site

52
test/key.pem Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCfl+m81U9+WLBn
PiJ1ZADnZC+BecXArguAdnbkgDlmfqe1eU6M0PBmXXFHEvJYOh2oYQZ7DMbuLLvo
DdjomXM4yb9Axu2KDHfflTcw3wHD3850ganf9rwVG460gkVfUGaRiokyCvEJKd05
BxKmx8Zh6G2tpetaDKm72ONYQFyaBCelYMzaBRpJ2kcPqk/gEhUSvQKVF4oDREm+
sTEkCTWsjUesiG0393t3psFa/SlZKsNXNrb+N6y9SElKVlQOT28r9YGiDfy5PiAQ
iFtQse2gqC4IhG2MGi0b4mu4Qa3+d2u3jsmn6Gqt92hKrbFOpz+Ci5GQ+zKXnE+j
/h40l1J3h/aTM5/qS7malAtXySMVzLQnJzai6IL+nBKbIOMCzP1Bns64Oo7YuuhV
Cx/sovrvgFF+8j4nLXOBgrx8llYB5cvgVYkNmccXtPzd7hZI6PHgZRGOd0uBKlZ+
OHeKhgdnz/a+pCUBwJe8JAzX7fzoZ+HLy26ADx2Uywh0Rjfj4gVOSZXaGS0ne0p5
PciN0xsWQdaAWPcVe6+YYRKS6l6qaWuGaPwVsZSvNgIMMPOgUn7+s2Bxh51DhqZT
4MDFUJWXFmTnyhHg3Cq7pWJR7pv2erv5q32b8iah5o+CP4W3FeZX1CcBuiIsrimh
vZZIa5W2vzpkLHbWgI2XGbx8yKRXowIDAQABAoICAENZb5lyB5cRRHh9XztdFYiQ
3f9s7UhP6qiu1aO+fPrFDm9mHwEMF7eLTYep9j3HYMazE3IQRU8z76SRW21lfJuF
gEGM8aeldV0UcnMcWXlY1J6ULaVHUb4yn/mLVE1R98cJyLYmqeutEB/F3VgmzJB6
7vYuI/EfkO2mLOMMXkfc4wJGpIyJRLvP8tcoj4bG+r+qphFXGrYgNmLUEiHcBRup
j4q/FCBfP2qSI90LI0zu3/rJK1aDFlHW1J8baWOUoBzUAX3rGzDth8iSUr7uJ5L6
Blsvz68lSM4QslbS2OOfcATJrE5Apex+kTOas023BPVJgwfFCmey3mUdk4+sIG+I
YpvrfUHBAbhRt1itGjjx/BH3HVtkwKsm3nNNgE8qS0qmziGVa60amLng0OgD4SOH
QKWrotcojYlWHywdHNAmiaWP+ZV9U0u4T59dkHhjqBoPuY64kNddRCLYItszqOmC
tamnflcoFPThdBnDpnXhLX+MnvQKIO2K2QTKjdYrkSMvEC/m96H49Vf4w+Fe3coc
SLsyMqHmYSG3+QtFN3JSmqDdBLQ+bFK0KZOk3IE2UK+lvabmtHuGLkYguur/fOnG
FvxcC+MIHHFrtSfqfvIrXeGFqe2Fb6DIKdMJFPV8iP5Px91A0GAGeabyktyya1n5
kk5nMJY3A7rTmnDO2PUBAoIBAQDSd+pvNGDf/We74+NODKQBcleCfF0NNzvRIuYH
9vel5jhyD28Y30XQS7+YBo3Y5Yo5pVlJ425Z5CveZOmr4VImuYG8LTbbGYY8ns3c
R/IdP1ByAcIa1eRGQNcKHid7RW4aEoFdvW3jZNj6/ukbJwd9ZHOt4HEUv5MOObn7
VXiXiwHjVy3X4uD5j0bHS4owd7S3Ygg0OneUzX3XJ+8qUgfU9B+lD3SbtsI4baoU
qUKcQc/0yXKvJvoz9oX2tjQOHqGrGwHdootc2CLnr0PMI7fKBwT3k63B+24xEGcx
OAPc5tUuiqIewiCn1oZbeCxmNnnimkAjDfG2KXMXj1R1G96BAoIBAQDCHnTNQhXH
tcfwnxj86E+Zd7ORS7iH/5eWvnspt2pnNCpufLFQlbdiL3d9ysSN9nw3+y15/o83
R79s7fLdC0+blc6q/ZQEdB1C+8YQa0nTlqqlZWVbRPjQUV+SfGssaZFEO4W7QWh3
hJL14HbWUFC3XSdMAeKTNOcgvfVvQ9M8HlJI1ZPo4Pe4rGNFIEsKUz/6HVE7HFgQ
OwcLSxZlSQSQ6/YErP/XJoyQUPjHldAjphV/rn28cvh2quoI35fbBuZu5pyKLXTg
RKbdWbeoIZTthRojpqnfqnIR+rPXIIiaMYhStdPshNUxLL7DX+/3yI05qq8R+yqa
2gLb2mJltuwjAoIBAF0WQI/ywK4Q7CKEBnLs0FT7d4z06EsCFOjI4KjBKIMtseVw
whhkGAKqnhDlRTObQmmAol81wgbsDiMMyvUEcUtDXQgXj12UinShYDd/cqxQ5omm
EW3BEHeqEfIdqCSzbqEFckY9lC6w2e8Zc4xY1M028psC28DrgmUWTxXEldOg3bLp
ShNj+1Eld46J8JLDPyCksTA4c89Sm8ffl75GDcS4PI7KqS59xKUki8cbnaRyz0Fb
H+gr+xmkfVfC+n8MOUDubwLR84Wa6sVCFWBio9UtCZteq8lSJUh6EsoIFl1LkxpE
orOr9LmG/mHSYwDKM1pwEtHuRuvkpUzUTeyF6QECggEAbm/vWZtgUsdbocyR5cix
CImuUlo2+MBz2KIz5c7grShjf4pXQpZ6x1Rj8d/7JRz3HM482Cv4BKZABNP3GMTH
nKeE9YjgvgvlXedpjovLa6JLIV/nYx6BQ9sXuXopaxIAQEZw1dDngx+ckGAMm+8D
jN5lbfugkMlHOTx5Nrzqn0hM3f0McjATHzCMJZayuoQUYNJvFWcRvuImJsmoSyVY
gK6Nv6lAwIHA9JXsg3f6+10Q3BxEkoMCUlj4XuX+OfDaBnwS0RX9aV4FZOcW8oNw
fBT+gwvdl08cKJht2lU7AiZt/UhO8j+8HobrXLHnDxw9JHKzuVIgsgqYF8ZNtrpz
6wKCAQEAqJc9xfNYL2jyJTVBvqX34Uct9xV1fq7z45B3gpPHj4tc0dBOIwPKSsu8
rGyefPuMDvbecJoaXqRZGQEcv03ecLVtHHDCsJcWavth9sCQJW9mnOxjOnLHdGnl
h2yIV4iVzNEx5XA1+0JRAAwFMXrC9LUZDTT61opjohMJSEySo/HVekoWG9A0bQGn
Ui+JbtLeuuCwwAGSgdAhVkjr27ANyFtjR63KVrDvSLbYFyZXKxrRYnZWs92Ho/g6
oTLnUSmkbLlhsUDjknAaNfp+enETSoPHcpkd7MeqVw2oYrLw6MCo4h2tBsIgtZEd
h6CgX4Gc3+bhvKx36XYxVwScPJUWsQ==
-----END PRIVATE KEY-----

View File

@@ -1,5 +0,0 @@
[app]
name = "markdown"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,3 +0,0 @@
# markdown
This example demonstrate Fes's ability to handle markdown routes.

View File

@@ -1,3 +0,0 @@
# Markdown!
**Fes** also supports markdown routes!

View File

@@ -1,5 +0,0 @@
[app]
name = "simple"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -1,5 +0,0 @@
# simple
This simple example shows the extensibility of the Fes framework. It shows the
you do not necessarily need to use the site object (although it is recommended)
you can define your own site, similar to how Lisps do things.

View File

@@ -1 +0,0 @@
return "Hello, World!"

BIN
test/static/seal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

46
test/www/index.lua Normal file
View File

@@ -0,0 +1,46 @@
local fes = require("fes")
local site = fes.fes()
local std = fes.std
local u = fes.util
site.copyright = std.link("https://fsdproject.org/", "fSD")
site:banner(table.concat {
std.h(1, "Testing Page"),
})
site:note(table.concat {
std.h(1, "Example syntax features"),
std.h(2, "Paragraphs"),
std.p("This is a paragraph."),
std.h(2, "Codeblocks"),
std.codeblock([[
#include <stdio.h>
int main() {
puts("Hello, World!");
return 0;
}]]),
std.h(2, "Inline Codeblocks"),
std.inline([[puts("Hello, World!");]]),
std.h(2, "Links"),
std.link("geminiprotocol.net/"),
std.h(2, "Lists"),
std.list {
"Item 1",
"Item 2",
"Item 3",
},
std.h(2, "Blockquotes"),
std.blockquote([["UNIX is very simple" - Dennis Ritchie
"GNU's Not UNIX" - Richard Stallman]]),
std.h(2, "Rules"),
std.rule(),
std.h(2, "Images"),
std.image("fluffy baby seal", "/static/seal.png"),
std.h(2, "Files"),
std.file("seal.png", "/static/seal.png"),
})
return site

3
todo
View File

@@ -1 +1,2 @@
footer: update link to direct to a local page for ISC
gemini: Add command-line options
http: https://git.erock.io