Compare commits

...

10 Commits

Author SHA1 Message Date
d2a832f451 add light-dark modes based on browser settings 2025-12-19 16:06:53 -05:00
fbcd2d8f06 refactor Start 2025-12-19 11:56:13 -05:00
311870683e fixes 2025-12-16 21:31:53 -05:00
e2c6f15e5b start beta versioning 2025-12-16 18:41:38 -05:00
10da72a1f6 doc: patch 2025-12-14 19:43:10 -05:00
522cbdece8 Merge pull request 'doc: first' (#4) from doc into main
Reviewed-on: #4
2025-12-14 19:41:43 -05:00
1427d0d780 doc: first 2025-12-14 19:40:55 -05:00
3cfc9b4aed Merge pull request 'rich-ui' (#3) from rich-ui into main
Reviewed-on: #3
2025-12-14 11:55:27 -05:00
2ff43cb8df restructure Start 2025-12-14 11:54:50 -05:00
f0e1f52ae2 factor out logging 2025-12-14 10:19:25 -05:00
9 changed files with 1002 additions and 145 deletions

View File

@@ -31,64 +31,269 @@ function M.fes(header, footer)
{{FAVICON}}
<title>{{TITLE}}</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; }
: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: #9aa6b1; }
h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); }
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; }
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::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); }
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 > *: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 strong { color: #f0f6f8; }
.muted { color: #9aa6b1; }
.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: #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; }
.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; }
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; }
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; }
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); }
.divider { margin: 26px 0; height: 1px; background: rgba(255,255,255,.1); }
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; }
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; }
.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 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; }
.right { text-align: right; float: right; }
</style>
</head>

View File

@@ -186,46 +186,6 @@ function M.site_authors()
return {}
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)
return '<span class="highlight">' .. (str or "") .. "</span>"
end

593
index.html Normal file
View 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 &lt;port&gt;</code></td>
<td>Set the server port</td>
</tr>
<tr>
<td><code>new &lt;project&gt;</code></td>
<td>Create a new projet called &lt;project&gt;</td>
</tr>
<tr>
<td><code>doc</code></td>
<td>Open this documention page</td>
</tr>
<tr>
<td><code>run &lt;project&gt;</code></td>
<td>Run the projet called &lt;project&gt;</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>&#169;</td> </tr>
<tr>
<td><code>Registered Trademark</code></td>
<td>&#174;</td>
</tr>
<tr>
<td><code>Trademark</code></td>
<td>&#8482;</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
View File

@@ -13,15 +13,20 @@ import (
"fes/src/doc"
"fes/src/new"
"fes/src/server"
"fes/src/version"
)
//go:embed core/*
var core embed.FS
//go:embed index.html
var documentation string
func init() {
config.Port = flag.Int("p", 3000, "Set the server port")
config.Color = flag.Bool("no-color", false, "Disable color output")
config.Core = core
config.Doc = documentation
}
func main() {
@@ -34,8 +39,14 @@ func main() {
fmt.Println("Options:")
flag.PrintDefaults()
}
showVersion := flag.Bool("version", false, "Show version and exit")
flag.Parse()
if *showVersion {
version.Version()
}
if *config.Color {
color.NoColor = true
}

View File

@@ -1,8 +1,12 @@
package config
import "embed"
import (
"embed"
"errors"
)
var Core embed.FS
var Doc string
var Port *int
var Color *bool
@@ -13,3 +17,5 @@ type MyConfig struct {
Authors []string `toml:"authors"`
} `toml:"app"`
}
var ErrRouteMiss = errors.New("not found")

View File

@@ -1,6 +1,7 @@
package doc
import (
"fes/src/config"
"fmt"
"os"
"path/filepath"
@@ -12,12 +13,8 @@ func Open() error {
fmt.Println("Opening documentation in browser")
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
}

View File

@@ -2,6 +2,7 @@ package server
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
@@ -13,13 +14,13 @@ import (
"time"
"fes/src/config"
"github.com/fatih/color"
"fes/src/ui"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
"html/template"
)
type reqData struct {
@@ -135,7 +136,7 @@ func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable {
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()
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 {
return "", err
return []byte(""), err
}
if L.GetTop() == 0 {
return "", nil
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 s, nil
return []byte(s), nil
}
return "", nil
return []byte(""), nil
}
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) {
@@ -350,26 +351,8 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
return b.String(), nil
}
func Start(dir string) error {
if err := os.Chdir(dir); err != nil {
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 := `
func generateNotFoundData(cfg *config.MyConfig) []byte {
notFoundData := []byte(`
<html>
<head><title>404 Not Found</title></head>
<body>
@@ -377,93 +360,123 @@ func Start(dir string) error {
<hr><center>fes</center>
</body>
</html>
`
`)
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
}
} else if _, err := os.Stat("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)
if entries, err := os.ReadDir("www"); 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 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 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)
}
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
p, ok := routes[path]
fmt.Printf("> %s ", basePath(filepath.Base(p)))
return routes
}
if !ok && strings.HasPrefix(path, "/archive") {
fsPath := "." + path
info, err := os.Stat(fsPath)
if err == nil && info.IsDir() {
if htmlStr, err := generateArchiveIndex(fsPath, path); err == nil {
w.Write([]byte(htmlStr))
return
}
}
func parseConfig() config.MyConfig {
tomlDocument, err := os.ReadFile("Fes.toml")
if err != nil {
ui.Error("failed to read Fes.toml", err)
os.Exit(1)
}
docStr := fixMalformedToml(string(tomlDocument))
var cfg config.MyConfig
if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil {
ui.Warning("failed to parse Fes.toml", err)
cfg.App.Authors = []string{"unknown"}
cfg.App.Name = "unknown"
cfg.App.Version = "unknown"
}
return cfg
}
func readArchive(w http.ResponseWriter, route string) {
fsPath := "." + route
if info, err := os.Stat(fsPath); err == nil && info.IsDir() {
if page, err := generateArchiveIndex(fsPath, route); err == nil {
w.Write([]byte(page))
}
}
}
func Start(dir string) error {
if err := os.Chdir(dir); err != nil {
return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err)
}
cfg := parseConfig()
notFoundData := generateNotFoundData(&cfg)
routes := loadDirs()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
route, ok := routes[r.URL.Path]
var err error = nil
/* defer won't update paramaters unless we do this. */
defer func() {
ui.Path(route, err)
}()
if !ok {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(notFoundData))
color.Yellow("not found")
err = config.ErrRouteMiss
route = r.URL.Path
if strings.HasPrefix(route, "/archive") {
readArchive(w, route)
} else {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(notFoundData))
}
return
}
params := make(map[string]string)
for k, val := range r.URL.Query() {
if len(val) > 0 {
params[k] = val[0]
for k, v := range r.URL.Query() {
if len(v) > 0 {
params[k] = v[0]
}
}
req := reqData{
path: path,
params: params,
}
var data []byte
var err error
if strings.HasSuffix(p, ".lua") {
var b string
b, err = loadLua(dir, p, &cfg, req)
data = []byte(b)
} else if strings.HasSuffix(p, ".md") {
data, err = os.ReadFile(p)
if strings.HasSuffix(route, ".lua") {
data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params})
} else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data)))
} else {
data, err = os.ReadFile(p)
data, err = os.ReadFile(route)
}
if err != nil {
http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError)
color.Red("bad")
return
}
w.Write(data)
color.Green("ok")
})
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)

57
src/ui/ui.go Normal file
View 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
View 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)
}