This commit is contained in:
2026-02-24 15:59:26 -05:00
parent ebd14a1b36
commit 1dd76cf1d2
24 changed files with 338 additions and 227 deletions

View File

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

View File

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

4
TODO Normal file
View File

@@ -0,0 +1,4 @@
* Fix Gemini rendering
* Add Gemini command-line options
* Better gemini errors
* Fix build steps newlines

View File

@@ -105,9 +105,8 @@ local default_gemini_footer = [[
{{COPYRIGHT}} {{COPYRIGHT}}
]] ]]
function M.fes(proto, header, footer) function M.fes(header, footer)
proto = proto or "http" local proto = std.proto
std.proto = proto
local config = {} local config = {}
local site_config = {} local site_config = {}
@@ -179,6 +178,14 @@ function M:build()
) )
return header .. table.concat(self.parts, "\n") .. footer 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") .. "\n\n" .. footer
end end
return table.concat(self.parts, "\n") return table.concat(self.parts, "\n")

View File

@@ -1,6 +1,7 @@
local M = {} local M = {}
M.proto = "http" M.proto = "http"
M.__fes_banner_set = false
local function isHttp() local function isHttp()
return M.proto == "http" return M.proto == "http"
@@ -10,18 +11,10 @@ local function isGemini()
return M.proto == "gemini" return M.proto == "gemini"
end end
local function esc(s)
s = s or ""
s = s:gsub("&", "&")
s = s:gsub("<", "&lt;")
s = s:gsub(">", "&gt;")
return s
end
M.p = function(s) M.p = function(s)
s = s or "" s = s or ""
if isHttp() then if isHttp() then
return "<p>" .. esc(s) .. "</p>" return "<p>" .. s .. "</p>"
elseif isGemini() then elseif isGemini() then
return s return s
end end
@@ -33,7 +26,7 @@ M.h = function(level, s)
if level > 6 then level = 6 end if level > 6 then level = 6 end
s = s or "" s = s or ""
if isHttp() then if isHttp() then
return "<h" .. level .. ">" .. esc(s) .. "</h" .. level .. ">" return "<h" .. level .. ">" .. s .. "</h" .. level .. ">"
elseif isGemini() then elseif isGemini() then
return string.rep("#", level) .. " " .. s return string.rep("#", level) .. " " .. s
end end
@@ -42,7 +35,7 @@ end
M.codeblock = function(s) M.codeblock = function(s)
s = s or "" s = s or ""
if isHttp() then if isHttp() then
return "<pre><code>" .. esc(s) .. "</code></pre>" return "<pre><code>" .. s .. "</code></pre>"
elseif isGemini() then elseif isGemini() then
return "```\n" .. s .. "\n```" return "```\n" .. s .. "\n```"
end end
@@ -51,17 +44,17 @@ end
M.inline = function(s) M.inline = function(s)
s = s or "" s = s or ""
if isHttp() then if isHttp() then
return "<code>" .. esc(s) .. "</code>" return "<code>" .. s .. "</code>"
elseif isGemini() then elseif isGemini() then
return "`" .. s .. "`" return "`" .. s .. "`"
end end
end end
M.link = function(text, url) M.link = function(url, text)
text = text or ""
url = url or "" url = url or ""
text = text or url
if isHttp() then if isHttp() then
return "<a href=\"" .. esc(url) .. "\">" .. esc(text) .. "</a>" return "<a href=\"" .. url .. "\">" .. text .. "</a>"
elseif isGemini() then elseif isGemini() then
return "=> " .. url .. " " .. text return "=> " .. url .. " " .. text
end end
@@ -73,7 +66,7 @@ M.list = function(items, ordered)
local tag = ordered and "ol" or "ul" local tag = ordered and "ol" or "ul"
local out = "<" .. tag .. ">" local out = "<" .. tag .. ">"
for _, v in ipairs(items) do for _, v in ipairs(items) do
out = out .. "<li>" .. esc(v) .. "</li>" out = out .. "<li>" .. v .. "</li>"
end end
out = out .. "</" .. tag .. ">" out = out .. "</" .. tag .. ">"
return out return out
@@ -93,7 +86,7 @@ end
M.blockquote = function(s) M.blockquote = function(s)
s = s or "" s = s or ""
if isHttp() then if isHttp() then
return "<blockquote>" .. esc(s) .. "</blockquote>" return "<blockquote>" .. s .. "</blockquote>"
elseif isGemini() then elseif isGemini() then
return "> " .. s return "> " .. s
end end
@@ -111,7 +104,7 @@ M.image = function(alt, src)
alt = alt or "" alt = alt or ""
src = src or "" src = src or ""
if isHttp() then if isHttp() then
return "<img src=\"" .. esc(src) .. "\" alt=\"" .. esc(alt) .. "\" />" return "<img src=\"" .. src .. "\" alt=\"" .. alt .. "\" />"
elseif isGemini() then elseif isGemini() then
return "=> " .. src .. " " .. alt return "=> " .. src .. " " .. alt
end end
@@ -121,7 +114,7 @@ M.file = function(text, url)
text = text or "" text = text or ""
url = url or "" url = url or ""
if isHttp() then if isHttp() then
return "<a href=\"" .. esc(url) .. "\" download>" .. esc(text) .. "</a>" return "<a href=\"" .. url .. "\" download>" .. text .. "</a>"
elseif isGemini() then elseif isGemini() then
return "=> " .. url .. " " .. text return "=> " .. url .. " " .. text
end end
@@ -130,53 +123,25 @@ end
M.note = function(text) M.note = function(text)
text = text or "" text = text or ""
if isHttp() then if isHttp() then
return "<div class=\"note\">" .. esc(text) .. "</div>" return "<div class=\"note\">" .. text .. "</div>"
elseif isGemini() then elseif isGemini() then
local width = 31 return "\n\n" .. text .. "\n\n"
end
end
local function wrap_line(line) M.banner = function (text)
local out = {} text = text or ""
local i = 1
local len = #line
if len == 0 then if M.__fes_banner_set then
out[#out + 1] = "" error("Page already contains header")
return out return ""
end end
while i <= len do M.__fes_banner_set = true
out[#out + 1] = line:sub(i, i + width - 1) if isHttp() then
i = i + width return "<div class=\"banner\">" .. text .. "</div>"
end elseif isGemini() then
return text .. "\n"
return out
end
local lines = {}
for line in (text .. "\n"):gmatch("(.-)\n") do
local wrapped = wrap_line(line)
for _, w in ipairs(wrapped) do
lines[#lines + 1] = w
end
end
local border = "+" .. string.rep("=", width + 2) .. "+"
local empty = "|" .. string.rep(" ", width + 2) .. "|"
local out = {}
out[#out + 1] = border
out[#out + 1] = empty
for _, line in ipairs(lines) do
local padding = width - #line
out[#out + 1] = "| " .. line .. string.rep(" ", padding) .. " |"
end
out[#out + 1] = empty
out[#out + 1] = border
return table.concat(out, "\n")
end end
end end

View File

@@ -105,11 +105,112 @@ All commands are run from the root of the project, from a terminal:
| Command | Action | | 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? ## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"), dir, dir) 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) ui.Hint("you can run this with `fes run %s`", dir)

View File

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

View File

@@ -25,17 +25,9 @@ func geminiHandler(w gemini.ResponseWriter, r *gemini.Request) {
route = r.URL.Path route = r.URL.Path
if strings.HasPrefix(route, "/archive") { if strings.HasPrefix(route, "/archive") {
w.Write([]byte("# error: not implemented")) err = readArchive(w, route, GEMINI)
// err = readArchive(w, route)
} else { } else {
w.WriteHeader(gemini.StatusNotFound, "StatusNotFound") w.WriteHeader(gemini.StatusNotFound, "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 return
} }
@@ -49,7 +41,7 @@ func geminiHandler(w gemini.ResponseWriter, r *gemini.Request) {
var data []byte var data []byte
if strings.HasSuffix(route, ".lua") { if strings.HasSuffix(route, ".lua") {
data, err = render(route, reqData{path: r.URL.Path, params: params}, &Sets) data, err = render(route, reqData{path: r.URL.Path, params: params}, GEMINI)
} else if strings.HasSuffix(route, ".md") { } else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route) data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data))) data = []byte(markdownToHTML(string(data)))

View File

@@ -26,7 +26,7 @@ func httpHandler(w http.ResponseWriter, r *http.Request) {
route = r.URL.Path route = r.URL.Path
if strings.HasPrefix(route, "/archive") { if strings.HasPrefix(route, "/archive") {
err = readArchive(w, route) err = readArchive(w, route, HTTP)
} else { } else {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`<html> w.Write([]byte(`<html>
@@ -49,7 +49,7 @@ func httpHandler(w http.ResponseWriter, r *http.Request) {
var data []byte var data []byte
if strings.HasSuffix(route, ".lua") { if strings.HasSuffix(route, ".lua") {
data, err = render(route, reqData{path: r.URL.Path, params: params}, &Sets) data, err = render(route, reqData{path: r.URL.Path, params: params}, HTTP)
} else if strings.HasSuffix(route, ".md") { } else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route) data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data))) data = []byte(markdownToHTML(string(data)))

View File

@@ -14,14 +14,7 @@ type reqData struct {
params map[string]string params map[string]string
} }
type DeclarativeSets struct { func render(luapath string, requestData reqData, protocol Protocols) ([]byte, error) {
protos struct {
http bool
gemini bool
}
}
func render(luapath string, requestData reqData, setBuffer *DeclarativeSets) ([]byte, error) {
L := lua.NewState() L := lua.NewState()
defer L.Close() defer L.Close()
@@ -74,6 +67,36 @@ func render(luapath string, requestData reqData, setBuffer *DeclarativeSets) ([]
panic("fes module did not return table") 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"
}
}()
stdTbl.RawSetString("proto", lua.LString(proto))
tbl.RawSetString("std", stdTbl)
bus := L.NewTable() bus := L.NewTable()
bus.RawSetString("url", lua.LString(requestData.path)) bus.RawSetString("url", lua.LString(requestData.path))

View File

@@ -11,8 +11,14 @@ import (
"sync" "sync"
) )
type Protocols int
const (
HTTP Protocols = iota
GEMINI
)
var Routes map[string]string var Routes map[string]string
var Sets DeclarativeSets
func Start(dir string) { func Start(dir string) {
if err := os.Chdir(dir); err != nil { if err := os.Chdir(dir); err != nil {

View File

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

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,8 +0,0 @@
local fes = require("fes")
local site = fes.fes()
-- site.copyright = fes.util.copyright("https://example.com", "vx-clutch")
site:h(1, "Hello, World!")
return site

View File

@@ -1,33 +0,0 @@
# default
```
fes new default
```
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
```
.
├── Fes.toml
├── README.md
└── www
└── index.lua
```
Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `fes run .` | Runs the project at `.` |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).

View File

@@ -1,18 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIC8zCCAdugAwIBAgIUWobZlV5Gp72Z4LUD/hjRb2aa+GgwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDIxNTAzMDgxOVoXDTI3MDIx
NTAzMDgxOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAxvNs7/1cJ/6kdlo7CAwIUc+d8L5cbqw3KKYMl/9JSnqp
HutIcl23LrF0ylClnAkTbuuDmzED73Z8788eaoIsjmwNA5yapkmDkjh/y8CRg1+2
8iEuneHAeKZosHGdfjBcOzLVPo713Mw2m3yXeeVLfn/FLUql3l/Au0xu+oVT4XB/
aZ//j3spgT4xIFggXMYchs9EW1pJpD4pnKDo+ZBATuAJjDy4OstGKzFiEiNSWfiI
K8VLM6V74xdEiojcyy2TCHDSYOIozsB5iQRV9PcXyyIEGw7wTx/o9wrkShff+pyn
seLJ644FnGRvEkZpTWg18NTC18JNLVGqmuSqbwzGZQIDAQABoz0wOzAaBgNVHREE
EzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFI/tgi60jQUeMqILEeZf7m80
MWB1MA0GCSqGSIb3DQEBCwUAA4IBAQATIwsWSuPBFb/n4q60QgScVIGjTIHTJGUT
di6ButyVug4zCltsMIw+VwfigRk77eyqZjbdm9Tmn/1cUTxLnNMBNyUPabojmf32
ItWGCLmI9QBW2/d8oK1rxLiDDQ5FwzWloeavwJC2E3xKy5xmcQicv1iTvpJnLRFJ
amyrY9dDVo0qAsLnnOmwc0OEnzpcYclegTOD9jUgEMJ00oLrYsWqXC8KvPaWcIu7
MiCj9j+U9ncU0fWE0WCOZr8VOgjtJeiHN1CLPOWbsSaZRWLBWlgF4AJGI2VXVM7d
BAb5y4cDIqmDnrTl3DUk8BlCnMdHopvl1ZrZWKgQJbfhvOagUv91
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDG82zv/Vwn/qR2
WjsIDAhRz53wvlxurDcopgyX/0lKeqke60hyXbcusXTKUKWcCRNu64ObMQPvdnzv
zx5qgiyObA0DnJqmSYOSOH/LwJGDX7byIS6d4cB4pmiwcZ1+MFw7MtU+jvXczDab
fJd55Ut+f8UtSqXeX8C7TG76hVPhcH9pn/+PeymBPjEgWCBcxhyGz0RbWkmkPimc
oOj5kEBO4AmMPLg6y0YrMWISI1JZ+IgrxUszpXvjF0SKiNzLLZMIcNJg4ijOwHmJ
BFX09xfLIgQbDvBPH+j3CuRKF9/6nKex4snrjgWcZG8SRmlNaDXw1MLXwk0tUaqa
5KpvDMZlAgMBAAECggEAFR1lvOzDWJ1OgB8gb8CzK1ehGBlj/vz5F6/T21flQ+nT
xCvNcxHeLK75ybUYdoCCFv4Y6CIiHEqThPIS9NPe/bibAvyebzKTK7QiYBIOf4Zr
iLQb2fbJMiTbLIrKX8erKj9BYZPTpTzpOMRW4UGEKydNWnq3MuwvrNE2YBFBb0YM
3RcSxs+nEkYs0sbZGtkqy2gYGbr0WcWHi3tNRWTT5FXM5VY84XY8QCPqDTj5fXys
DwDFQDBJc0l/IYRcaen+4UNliVaJRto/ZZaqhwnPra1d16PLYhWmfAkYKgWCEIhw
+b/+mV+6oUnORGbmq1TiSzZ9U9WqNwSo/8ZoNC8WywKBgQDm7SK2zFyXx4SdnVfL
XikJCYLdnsLaaP/Z7iCZgt7oaHUDg6Eb/SAqdEB/YPcCHXox6ChUOqHe7ee7ROCk
1wD3xI+kV4E9yZqs2zeRJrv8W8Q0JjJXdVrXy6vFQ0z/132QNXLJpr7KXGctE9zX
XheT+yisgJQSd6O7HX1Ow67EKwKBgQDcjX7tThSw+dloyEywbi3dA8VmGVuH3UPk
3zgD6dEA/xt/OpD3LgDgHOLIL+lbR4LAfxjS8RTHGOON8iVcCAi6k7gAf+0WU2Uh
6GkA4gUM6mx1zSl7k4/vmJa1WpxG11bCdWPvNt3X0cvYMGNPfhTITqz4F81M6+9p
ZEmaCGaHrwKBgGGmSzSjbFAeZXzE6TgtJAsXQ4h1tw3msrI0GQLxLVN3wGtxAPK1
8iEhsZhrp2f0kRSDiHI9rO95CLHO6XOrG1SqgNdMzXEUTFzmAjRV/c4z+97VfBox
nO19ybALyoaxV/5gK58L7MfjlRmhuZQ0zKGd5lAzuumoP8tDKBbjdoarAoGAcNJ9
DH21vfaBjcVw3YvvMDE+qITuOqkokwrRB8dzIBRgB4x5HcjNr9d29zrzH7uMGlap
5zZmD5ceyL0G+XYuqOrp5G+MY7BTeq3+EPKN7NZ6lyRVRR7uMX2YEruAWAjOG/mb
HoKtpzpuEXBnTQHNNc5xUxQx9Fh5ByvDLuV/NYcCgYEAy/An+fPP6Lkl4nwbcnbP
npAimzB6z25ftFeNMfggJYOukQomAeuwS5QYdLvqtPdqjtrqRQJrXFW9q0Nmt5HM
h0WuMCrKDWfYdZbZ8E6y3zqUXb5J66M2mcu+8ED6zUvktOBHXgIS7YnXYf46illx
3+8QDk1ufotloNSokoM/BTw=
-----END PRIVATE KEY-----

View File

@@ -1,16 +0,0 @@
[req]
default_bits = 2048
prompt = no
default_md = sha256
x509_extensions = v3_req
distinguished_name = dn
[dn]
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1

View File

@@ -1,10 +0,0 @@
local fes = require("fes")
local site = fes.fes("gemini")
-- site.copyright = fes.util.copyright("https://example.com", "vx-clutch")
site:h(1, "Hello, World!")
site:note("Hello, World!. djsaklllllll\nlllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll")
return site

View File

@@ -1,7 +0,0 @@
# error
This is expected to return the following error:
```
Error loading page: www/index.lua line:6(column:6) near 'return': syntax error
```

View File

@@ -1,6 +0,0 @@
local fes = require("fes")
local site = fes.fes()
site.
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-----

BIN
test/static/seal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

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

@@ -0,0 +1,49 @@
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
<br>
"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