Compare commits
10 Commits
422db7490a
...
d2a832f451
| Author | SHA1 | Date | |
|---|---|---|---|
| d2a832f451 | |||
| fbcd2d8f06 | |||
| 311870683e | |||
| e2c6f15e5b | |||
| 10da72a1f6 | |||
| 522cbdece8 | |||
| 1427d0d780 | |||
| 3cfc9b4aed | |||
| 2ff43cb8df | |||
| f0e1f52ae2 |
261
core/builtin.lua
261
core/builtin.lua
@@ -31,64 +31,269 @@ function M.fes(header, footer)
|
|||||||
{{FAVICON}}
|
{{FAVICON}}
|
||||||
<title>{{TITLE}}</title>
|
<title>{{TITLE}}</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { min-height: 100%; margin: 0; padding: 0; background: #0f1113; color: #e6eef3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
: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; }
|
body { padding: 36px; }
|
||||||
|
|
||||||
.container { max-width: 830px; margin: 0 auto; }
|
.container { max-width: 830px; margin: 0 auto; }
|
||||||
|
|
||||||
.container > *:not(.banner) { margin: 28px 0; }
|
.container > *:not(.banner) { margin: 28px 0; }
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 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; }
|
h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; }
|
||||||
|
|
||||||
h2 { font-size: 32px; margin: 26px 0 14px; }
|
h2 { font-size: 32px; margin: 26px 0 14px; }
|
||||||
|
|
||||||
h3 { font-size: 26px; margin: 22px 0 12px; }
|
h3 { font-size: 26px; margin: 22px 0 12px; }
|
||||||
|
|
||||||
h4 { font-size: 20px; margin: 18px 0 10px; }
|
h4 { font-size: 20px; margin: 18px 0 10px; }
|
||||||
|
|
||||||
h5 { font-size: 16px; margin: 16px 0 8px; }
|
h5 { font-size: 16px; margin: 16px 0 8px; }
|
||||||
h6 { font-size: 14px; margin: 14px 0 6px; color: #9aa6b1; }
|
|
||||||
|
h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); }
|
||||||
|
|
||||||
p { margin: 14px 0; }
|
p { margin: 14px 0; }
|
||||||
a { color: #68a6ff; text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; }
|
|
||||||
.hidden { color: #dfe9ee; text-decoration: none; }
|
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; }
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
summary { cursor: pointer; }
|
summary { cursor: pointer; }
|
||||||
details { background: #1a1c20; border: 1px solid rgba(255,255,255,.06); border-radius: 4px; padding: 14px 16px; margin: 16px 0; }
|
|
||||||
details summary { list-style: none; font-weight: 600; color: #e6eef3; display: flex; align-items: center; }
|
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::-webkit-details-marker { display: none; }
|
||||||
details summary::before { content: "▸"; margin-right: 8px; transition: transform .15s ease; color: #68a6ff; }
|
|
||||||
|
details summary::before {
|
||||||
|
content: "▸";
|
||||||
|
margin-right: 8px;
|
||||||
|
transition: transform .15s ease;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
details[open] summary::before { transform: rotate(90deg); }
|
details[open] summary::before { transform: rotate(90deg); }
|
||||||
summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: #9aa6b1; }
|
|
||||||
|
summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); }
|
||||||
|
|
||||||
details[open] summary::after { content: "Collapse"; }
|
details[open] summary::after { content: "Collapse"; }
|
||||||
|
|
||||||
details > *:not(summary) { margin-top: 12px; }
|
details > *:not(summary) { margin-top: 12px; }
|
||||||
.note, pre, code { background: #1a1c20; border: 1px solid rgba(255,255,255,.06); }
|
|
||||||
.note { padding: 20px; border-radius: 4px; background: #1a1c20; border: 1px solid rgba(255,255,255,.06); margin: 28px 0; color: #dfe9ee; }
|
.note, pre, code {
|
||||||
.note strong { color: #f0f6f8; }
|
background: var(--note-bg);
|
||||||
.muted { color: #9aa6b1; }
|
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; }
|
.lead { font-size: 15px; margin-top: 8px; }
|
||||||
|
|
||||||
.callout { display: block; margin: 12px 0; }
|
.callout { display: block; margin: 12px 0; }
|
||||||
.small { font-size: 13px; color: #9aa6b1; margin-top: 6px; }
|
|
||||||
.highlight { font-weight: 700; color: #cde7ff; }
|
.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; }
|
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; }
|
|
||||||
|
.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; }
|
ul.tl li { padding: 10px; width: fit-content; }
|
||||||
|
|
||||||
li { margin: 6px 0; }
|
li { margin: 6px 0; }
|
||||||
code { padding: 3px 7px; border-radius: 3px; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: .9em; color: #cde7ff; }
|
|
||||||
pre { padding: 20px; border-radius: 4px; margin: 14px 0; overflow-x: auto; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: 14px; line-height: 1.6; }
|
code {
|
||||||
|
padding: 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; }
|
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
||||||
blockquote { border-left: 3px solid #68a6ff; padding-left: 18px; margin: 14px 0; color: #dfe9ee; font-style: italic; }
|
|
||||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.1); margin: 26px 0; }
|
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; }
|
img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; margin: 14px 0; }
|
table { width: 100%; border-collapse: collapse; margin: 14px 0; }
|
||||||
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid rgba(255,255,255,.06); }
|
|
||||||
th { background: #1a1c20; font-weight: 600; color: #f0f6f8; }
|
th, td {
|
||||||
tr:hover { background: rgba(255,255,255,.02); }
|
padding: 12px 16px;
|
||||||
.divider { margin: 26px 0; height: 1px; background: rgba(255,255,255,.1); }
|
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; }
|
.section { margin-top: 36px; }
|
||||||
|
|
||||||
.links { margin: 12px 0; }
|
.links { margin: 12px 0; }
|
||||||
.links a { display: inline-block; margin: 0 14px 6px 0; }
|
|
||||||
strong, b { font-weight: 600; color: #f0f6f8; }
|
.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; }
|
em, i { font-style: italic; }
|
||||||
|
|
||||||
.center { display: flex; justify-content: center; align-items: center; }
|
.center { display: flex; justify-content: center; align-items: center; }
|
||||||
.banner { width: 100%; box-sizing: border-box; text-align: center; background: #1a1c20; padding: 20px; border: 1px solid rgba(255,255,255,.06); border-bottom-right-radius: 8px; border-bottom-left-radius: 8px; color: #e6eef3; margin: -36px 0 28px 0; box-shadow: 0 0.2em 0.6em rgba(0,0,0,.4); }
|
|
||||||
|
.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 { margin-left: auto; margin-right: auto; }
|
||||||
.nav a { color: #cde7ff; }
|
|
||||||
.footer { background: #1a1c20; padding: 20px 0; border-top: 1px solid rgba(255,255,255,.1); font-size: 14px; color: #d4dde3; display: flex; justify-content: center; align-items: center; gap: 24px; margin-top: 28px !important; margin-bottom: 0; }
|
.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; }
|
.left { text-align: left; float: left; }
|
||||||
|
|
||||||
.right { text-align: right; float: right; }
|
.right { text-align: right; float: right; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
40
core/std.lua
40
core/std.lua
@@ -186,46 +186,6 @@ function M.site_authors()
|
|||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Join array with separator
|
|
||||||
function M.join(arr, sep)
|
|
||||||
arr = arr or {}
|
|
||||||
sep = sep or ", "
|
|
||||||
local result = {}
|
|
||||||
for _, v in ipairs(arr) do
|
|
||||||
table.insert(result, tostring(v))
|
|
||||||
end
|
|
||||||
return table.concat(result, sep)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Trim whitespace
|
|
||||||
function M.trim(str)
|
|
||||||
str = tostring(str or "")
|
|
||||||
return str:match("^%s*(.-)%s*$")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Table HTML generator
|
|
||||||
function M.table(headers, rows)
|
|
||||||
headers = headers or {}
|
|
||||||
rows = rows or {}
|
|
||||||
|
|
||||||
local html = "<table><thead><tr>"
|
|
||||||
for _, header in ipairs(headers) do
|
|
||||||
html = html .. "<th>" .. tostring(header) .. "</th>"
|
|
||||||
end
|
|
||||||
html = html .. "</tr></thead><tbody>"
|
|
||||||
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
html = html .. "<tr>"
|
|
||||||
for _, cell in ipairs(row) do
|
|
||||||
html = html .. "<td>" .. tostring(cell) .. "</td>"
|
|
||||||
end
|
|
||||||
html = html .. "</tr>"
|
|
||||||
end
|
|
||||||
|
|
||||||
html = html .. "</tbody></table>"
|
|
||||||
return html
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.highlight(str)
|
function M.highlight(str)
|
||||||
return '<span class="highlight">' .. (str or "") .. "</span>"
|
return '<span class="highlight">' .. (str or "") .. "</span>"
|
||||||
end
|
end
|
||||||
|
|||||||
593
index.html
Normal file
593
index.html
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Documentation</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #0f1113;
|
||||||
|
color: #e6eef3;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 830px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 26px 0 14px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.1);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 26px;
|
||||||
|
margin: 22px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
color: #9aa6b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
margin: 28px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #1a1c20;
|
||||||
|
border: 1px solid rgba(255,255,255,.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #68a6ff;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 14px 0;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
||||||
|
font-size: .9em;
|
||||||
|
color: #cde7ff;
|
||||||
|
background: #1a1c20;
|
||||||
|
border: 1px solid rgba(255,255,255,.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 14px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #1a1c20;
|
||||||
|
border: 1px solid rgba(255,255,255,.06);
|
||||||
|
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;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid #68a6ff;
|
||||||
|
padding-left: 18px;
|
||||||
|
margin: 14px 0;
|
||||||
|
color: #dfe9ee;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #1a1c20;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: rgba(255,255,255,.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 48px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid rgba(255,255,255,.1);
|
||||||
|
color: #9aa6b1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Documentation</h1>
|
||||||
|
<p>Fes: A lightweight, static, and opinionated microframework.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<h2>Contents</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#introduction">Introduction</a></li>
|
||||||
|
<li><a href="#installation">Installation</a></li>
|
||||||
|
<li><a href="#usage">Usage</a></li>
|
||||||
|
<li><a href="#cli-reference">Cli Reference</a></li>
|
||||||
|
<li><a href="#reference">Reference</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="introduction">
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>Fes, or Free Easy Site, is a microframework used for small static sites. It is not designed for complex web applications and that is why it is good. Yes, I hate modern web and that is the reason this exists.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="installation">
|
||||||
|
<h2>Installation</h2>
|
||||||
|
<pre><code>git clone https://git.vxserver.dev/fSD/fes</code></pre>
|
||||||
|
<pre><code>cd fes</code></pre>
|
||||||
|
<pre><code>go install fes</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="usage">
|
||||||
|
<h2>Usage</h2>
|
||||||
|
<p>Typical workflows and examples.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Creating project</li>
|
||||||
|
<li>Hosting websites</li>
|
||||||
|
<li>Generating websites</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="cli-reference">
|
||||||
|
<h2>Cli Reference</h2>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>--help</code></td>
|
||||||
|
<td>Display help information</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--no-color</code></td>
|
||||||
|
<td>Disable color output</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>-p <port></code></td>
|
||||||
|
<td>Set the server port</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>new <project></code></td>
|
||||||
|
<td>Create a new projet called <project></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>doc</code></td>
|
||||||
|
<td>Open this documention page</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>run <project></code></td>
|
||||||
|
<td>Run the projet called <project></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="reference">
|
||||||
|
<h2>Reference</h2>
|
||||||
|
All <code>std</code> functions have binding for the site and can be used like so: <code>site:h1("Hello, World!")</code>, where site is the site object.
|
||||||
|
<h3>Builtin</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>fes()</code></td>
|
||||||
|
<td>Generate a site object</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>:custom()</code></td>
|
||||||
|
<td>Add a custom string to the site body</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>markdown_to_html(str: string)</code></td>
|
||||||
|
<td>Returns generated HTML from provided Markdown string.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Std</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.fes_version()</code></td>
|
||||||
|
<td>Get the current version of fes.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.site_version()</code></td>
|
||||||
|
<td>Get the current version of the site, defined in <code>Fes.toml</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.site_name()</code></td>
|
||||||
|
<td>Get the current name of the site, defined in <code>Fes.toml</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.site_title()</code></td>
|
||||||
|
<td>Get the current name of the site, defined in <code>Fes.toml</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.site_authors()</code></td>
|
||||||
|
<td>Get a table of the authors of the site, defined in <code>Fes.toml</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.join</code></td>
|
||||||
|
<td>Get a table of the authors of the site, defined in <code>Fes.toml</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.a(link: string, str: string)</code></td>
|
||||||
|
<td>Returns an anchor tag.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.ha(link: string, str: string)</code></td>
|
||||||
|
<td>Returns an anchor tag with sytiling to make it hidden.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.external(link: string, str: string)</code></td>
|
||||||
|
<td>Returns an anchor tag that opens up in a new tab.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.note(str: string)</code></td>
|
||||||
|
<td>Returns a note object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.muted(str: string)</code></td>
|
||||||
|
<td>Returns a muted object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.callout(str: string)</code></td>
|
||||||
|
<td>Returns a callout object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h1(str: string)</code></td>
|
||||||
|
<td>Returns a header level 1 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h2(str: string)</code></td>
|
||||||
|
<td>Returns a header level 2 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h3(str: string)</code></td>
|
||||||
|
<td>Returns a header level 3 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h4(str: string)</code></td>
|
||||||
|
<td>Returns a header level 4 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h5(str: string)</code></td>
|
||||||
|
<td>Returns a header level 5 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.h6(str: string)</code></td>
|
||||||
|
<td>Returns a header level 6 object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.p(str: string)</code></td>
|
||||||
|
<td>Returns a paragraph object.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.pre(str: string)</code></td>
|
||||||
|
<td>Returns preformated text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.code(str: string)</code></td>
|
||||||
|
<td>Returns a codeblock.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.ul(items: {...strings})</code></td>
|
||||||
|
<td>Generates an unordered list from a table of strings.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.ol(items: {...strings})</code></td>
|
||||||
|
<td>Generates an ordered list from a table of strings.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.tl(items: {...strings})</code></td>
|
||||||
|
<td>Generates a table from a table of strings.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.blockquote(str: string)</code></td>
|
||||||
|
<td>Returns a blockquote.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.hr()</code></td>
|
||||||
|
<td>Returns a horizonal rule.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.img(src: string, alt: string)</code></td>
|
||||||
|
<td>Returns an image at location source with given alternative text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.strong(str: string)</code></td>
|
||||||
|
<td>Returns bolded text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.em(str: string)</code></td>
|
||||||
|
<td>Returns italicized text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.br()</code></td>
|
||||||
|
<td>Returns a break.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.div(content: string, class: string)</code></td>
|
||||||
|
<td>Returns a div of class <code>class</code> with content of <code>content</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.spa(content: string, class: string)</code></td>
|
||||||
|
<td>Returns a span of class <code>class</code> with content of <code>content</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.escape(str: string)</code></td>
|
||||||
|
<td>Returns an html escaped string.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.highlight(str: string)</code></td>
|
||||||
|
<td>Returns highlighted text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.banner(str: string)</code></td>
|
||||||
|
<td>Returns a banner that is attached to the top of the site.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.center(str: string)</code></td>
|
||||||
|
<td>Returns centered text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.nav(link: string, str: string)</code></td>
|
||||||
|
<td>Returns a speical navigation link, used for in-site traversal.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>std.rl(r: string, l: string)</code></td>
|
||||||
|
<td>Right and light alight content.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Symbol</h3>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Symbol</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>symbol.copyright</code></td>
|
||||||
|
<td>©</td> </tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Registered Trademark</code></td>
|
||||||
|
<td>®</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Trademark</code></td>
|
||||||
|
<td>™</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Util</h3>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>util.cc(tbl: {...strings})</code></td>
|
||||||
|
<td>Concatenate a table of strings into a single string.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>util.copyright(link: string, holder: string)</code></td>
|
||||||
|
<td>Used when setting the website copyright holder.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Dkjson</h3>
|
||||||
|
This is a third party object and is best documented <a href="https://dkolf.de/dkjson-lua/documentation" target="_blank">here</a>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>dkjson.encode(obj: {...any})</code></td>
|
||||||
|
<td>Serilize a Lua table into JSON.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>dkjson.decode(obj: {...any})</code></td>
|
||||||
|
<td>Deserilize JSON into a Lua table.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Bus</h3>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>bus.url</code></td>
|
||||||
|
<td>The current url that was given.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>bus.params</code></td>
|
||||||
|
<td>A table of url parameters if any. Ex: ?foo=bar*baz=foobar</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Site</h3>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>site.version</code></td>
|
||||||
|
<td>The version of the website found in the Fes.toml</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>site.name</code></td>
|
||||||
|
<td>The name of the website found in the Fes.toml</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>site.authors</code></td>
|
||||||
|
<td>A table of all authors defined in Fes.toml</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>App</h3>
|
||||||
|
Fes's <code>app</code> module is a special table
|
||||||
|
because it contains user defined functions. These
|
||||||
|
functions are defined in the <code>include/</code> For
|
||||||
|
example if you define a <code>include/hello.lua</code>
|
||||||
|
file with given contents: <pre><code>local hello = {}
|
||||||
|
|
||||||
|
hello.render(std) return std.h1("Hello, World!") end
|
||||||
|
|
||||||
|
return hello</pre></code> This can be called from another with,
|
||||||
|
<code>fes.app.hello.render(fes.std)</code>. Do with this
|
||||||
|
as you will.
|
||||||
|
<h3>Speical Directories</h3>
|
||||||
|
<table> <thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>www/</code></td>
|
||||||
|
<td>The main website is
|
||||||
|
contained here, this is
|
||||||
|
where www/index.lua
|
||||||
|
lives.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>static/</code></td>
|
||||||
|
<td>All static content should
|
||||||
|
be placed here and can
|
||||||
|
be accessed at
|
||||||
|
<code>/static/path-to-file</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>include/</code></td>
|
||||||
|
<td>Contains lua files that are
|
||||||
|
preloaded and globally
|
||||||
|
accessible.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>archive/</code></td>
|
||||||
|
<td>Files here can be viewed in
|
||||||
|
a file browser like
|
||||||
|
format at
|
||||||
|
<code>/archive/path-to-dir</code>.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Last updated: 2025-12-16</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
main.go
11
main.go
@@ -13,15 +13,20 @@ import (
|
|||||||
"fes/src/doc"
|
"fes/src/doc"
|
||||||
"fes/src/new"
|
"fes/src/new"
|
||||||
"fes/src/server"
|
"fes/src/server"
|
||||||
|
"fes/src/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed core/*
|
//go:embed core/*
|
||||||
var core embed.FS
|
var core embed.FS
|
||||||
|
|
||||||
|
//go:embed index.html
|
||||||
|
var documentation string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
config.Port = flag.Int("p", 3000, "Set the server port")
|
config.Port = flag.Int("p", 3000, "Set the server port")
|
||||||
config.Color = flag.Bool("no-color", false, "Disable color output")
|
config.Color = flag.Bool("no-color", false, "Disable color output")
|
||||||
config.Core = core
|
config.Core = core
|
||||||
|
config.Doc = documentation
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,8 +39,14 @@ func main() {
|
|||||||
fmt.Println("Options:")
|
fmt.Println("Options:")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showVersion := flag.Bool("version", false, "Show version and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *showVersion {
|
||||||
|
version.Version()
|
||||||
|
}
|
||||||
|
|
||||||
if *config.Color {
|
if *config.Color {
|
||||||
color.NoColor = true
|
color.NoColor = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "embed"
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
var Core embed.FS
|
var Core embed.FS
|
||||||
|
var Doc string
|
||||||
var Port *int
|
var Port *int
|
||||||
var Color *bool
|
var Color *bool
|
||||||
|
|
||||||
@@ -13,3 +17,5 @@ type MyConfig struct {
|
|||||||
Authors []string `toml:"authors"`
|
Authors []string `toml:"authors"`
|
||||||
} `toml:"app"`
|
} `toml:"app"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrRouteMiss = errors.New("not found")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package doc
|
package doc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fes/src/config"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,12 +13,8 @@ func Open() error {
|
|||||||
fmt.Println("Opening documentation in browser")
|
fmt.Println("Opening documentation in browser")
|
||||||
|
|
||||||
tmpFile := filepath.Join(os.TempDir(), "doc.html")
|
tmpFile := filepath.Join(os.TempDir(), "doc.html")
|
||||||
content := `<html><body><pre>
|
|
||||||
This feature is not implemented yet. It will be once the doc site
|
|
||||||
is up and running, for now read through the core/ files and examples.
|
|
||||||
</pre></body></html>`
|
|
||||||
|
|
||||||
if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(config.Doc), 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,13 +14,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fes/src/config"
|
"fes/src/config"
|
||||||
"github.com/fatih/color"
|
"fes/src/ui"
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
"github.com/gomarkdown/markdown/html"
|
"github.com/gomarkdown/markdown/html"
|
||||||
"github.com/gomarkdown/markdown/parser"
|
"github.com/gomarkdown/markdown/parser"
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
"html/template"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type reqData struct {
|
type reqData struct {
|
||||||
@@ -135,7 +136,7 @@ func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable {
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadLua(luaDir string, entry string, cfg *config.MyConfig, requestData reqData) (string, error) {
|
func loadLua(entry string, cfg *config.MyConfig, requestData reqData) ([]byte, error) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
|
|
||||||
@@ -238,26 +239,26 @@ func loadLua(luaDir string, entry string, cfg *config.MyConfig, requestData reqD
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := L.DoFile(entry); err != nil {
|
if err := L.DoFile(entry); err != nil {
|
||||||
return "", err
|
return []byte(""), err
|
||||||
}
|
}
|
||||||
|
|
||||||
if L.GetTop() == 0 {
|
if L.GetTop() == 0 {
|
||||||
return "", nil
|
return []byte(""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
L.SetGlobal("__fes_result", L.Get(-1))
|
L.SetGlobal("__fes_result", L.Get(-1))
|
||||||
if err := L.DoString("return tostring(__fes_result)"); err != nil {
|
if err := L.DoString("return tostring(__fes_result)"); err != nil {
|
||||||
L.GetGlobal("__fes_result")
|
L.GetGlobal("__fes_result")
|
||||||
if s := L.ToString(-1); s != "" {
|
if s := L.ToString(-1); s != "" {
|
||||||
return s, nil
|
return []byte(s), nil
|
||||||
}
|
}
|
||||||
return "", nil
|
return []byte(""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := L.ToString(-1); s != "" {
|
if s := L.ToString(-1); s != "" {
|
||||||
return s, nil
|
return []byte(s), nil
|
||||||
}
|
}
|
||||||
return "", nil
|
return []byte(""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
|
func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
|
||||||
@@ -350,26 +351,8 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
|
|||||||
return b.String(), nil
|
return b.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(dir string) error {
|
func generateNotFoundData(cfg *config.MyConfig) []byte {
|
||||||
if err := os.Chdir(dir); err != nil {
|
notFoundData := []byte(`
|
||||||
return fmt.Errorf("failed to change directory to %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
dir = "."
|
|
||||||
|
|
||||||
tomlDocument, err := os.ReadFile("Fes.toml")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read Fes.toml: %w", err)
|
|
||||||
}
|
|
||||||
docStr := fixMalformedToml(string(tomlDocument))
|
|
||||||
var cfg config.MyConfig
|
|
||||||
if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil {
|
|
||||||
fmt.Printf("Warning: failed to parse Fes.toml: %v\n", err)
|
|
||||||
cfg.App.Authors = []string{"unknown"}
|
|
||||||
cfg.App.Name = "unknown"
|
|
||||||
cfg.App.Version = "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
notFoundData := `
|
|
||||||
<html>
|
<html>
|
||||||
<head><title>404 Not Found</title></head>
|
<head><title>404 Not Found</title></head>
|
||||||
<body>
|
<body>
|
||||||
@@ -377,93 +360,123 @@ func Start(dir string) error {
|
|||||||
<hr><center>fes</center>
|
<hr><center>fes</center>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`
|
`)
|
||||||
if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil {
|
if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil {
|
||||||
if nf, err := loadLua(dir, "www/404.lua", &cfg, reqData{}); err == nil {
|
if nf, err := loadLua("www/404.lua", cfg, reqData{}); err == nil {
|
||||||
notFoundData = nf
|
notFoundData = nf
|
||||||
}
|
}
|
||||||
} else if _, err := os.Stat("www/404.html"); err == nil {
|
} else if _, err := os.Stat("www/404.html"); err == nil {
|
||||||
if buf, err := os.ReadFile("www/404.html"); err == nil {
|
if buf, err := os.ReadFile("www/404.html"); err == nil {
|
||||||
notFoundData = string(buf)
|
notFoundData = buf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return notFoundData
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDirs() map[string]string {
|
||||||
routes := make(map[string]string)
|
routes := make(map[string]string)
|
||||||
|
|
||||||
if entries, err := os.ReadDir("www"); err == nil {
|
if entries, err := os.ReadDir("www"); err == nil {
|
||||||
if err := handleDir(entries, "www", routes, "", false); err != nil {
|
if err := handleDir(entries, "www", routes, "", false); err != nil {
|
||||||
fmt.Printf("Warning: failed to handle www directory: %v\n", err)
|
ui.Warning("failed to handle www directory", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if entries, err := os.ReadDir("static"); err == nil {
|
if entries, err := os.ReadDir("static"); err == nil {
|
||||||
if err := handleDir(entries, "static", routes, "/static", true); err != nil {
|
if err := handleDir(entries, "static", routes, "/static", true); err != nil {
|
||||||
fmt.Printf("Warning: failed to handle static directory: %v\n", err)
|
ui.Warning("failed to handle static directory", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if entries, err := os.ReadDir("archive"); err == nil {
|
if entries, err := os.ReadDir("archive"); err == nil {
|
||||||
if err := handleDir(entries, "archive", routes, "/archive", true); err != nil {
|
if err := handleDir(entries, "archive", routes, "/archive", true); err != nil {
|
||||||
fmt.Printf("Warning: failed to handle archive directory: %v\n", err)
|
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) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
route, ok := routes[r.URL.Path]
|
||||||
p, ok := routes[path]
|
|
||||||
fmt.Printf("> %s ", basePath(filepath.Base(p)))
|
|
||||||
|
|
||||||
if !ok && strings.HasPrefix(path, "/archive") {
|
var err error = nil
|
||||||
fsPath := "." + path
|
|
||||||
info, err := os.Stat(fsPath)
|
/* defer won't update paramaters unless we do this. */
|
||||||
if err == nil && info.IsDir() {
|
defer func() {
|
||||||
if htmlStr, err := generateArchiveIndex(fsPath, path); err == nil {
|
ui.Path(route, err)
|
||||||
w.Write([]byte(htmlStr))
|
}()
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
err = config.ErrRouteMiss
|
||||||
|
route = r.URL.Path
|
||||||
|
|
||||||
|
if strings.HasPrefix(route, "/archive") {
|
||||||
|
readArchive(w, route)
|
||||||
|
} else {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
w.Write([]byte(notFoundData))
|
w.Write([]byte(notFoundData))
|
||||||
color.Yellow("not found")
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
for k, val := range r.URL.Query() {
|
for k, v := range r.URL.Query() {
|
||||||
if len(val) > 0 {
|
if len(v) > 0 {
|
||||||
params[k] = val[0]
|
params[k] = v[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req := reqData{
|
|
||||||
path: path,
|
|
||||||
params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
if strings.HasSuffix(route, ".lua") {
|
||||||
|
data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params})
|
||||||
if strings.HasSuffix(p, ".lua") {
|
} else if strings.HasSuffix(route, ".md") {
|
||||||
var b string
|
data, err = os.ReadFile(route)
|
||||||
b, err = loadLua(dir, p, &cfg, req)
|
|
||||||
data = []byte(b)
|
|
||||||
} else if strings.HasSuffix(p, ".md") {
|
|
||||||
data, err = os.ReadFile(p)
|
|
||||||
data = []byte(markdownToHTML(string(data)))
|
data = []byte(markdownToHTML(string(data)))
|
||||||
} else {
|
} else {
|
||||||
data, err = os.ReadFile(p)
|
data, err = os.ReadFile(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError)
|
||||||
color.Red("bad")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
color.Green("ok")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)
|
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)
|
||||||
|
|||||||
57
src/ui/ui.go
Normal file
57
src/ui/ui.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
15
src/version/version.go
Normal file
15
src/version/version.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user