Files
fes/core/builtin.lua
2025-11-30 18:48:30 -05:00

148 lines
6.7 KiB
Lua

local std = require("core.std")
local M = {}
M.__index = M
local function encode(str)
return str:gsub("([^%w%-%_%.%~])", function(c)
return string.format("%%%02X", string.byte(c)) end) end
function M.fes(header, footer)
local config = {}
local site_config = {}
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config then
config = fes_mod.config
if config.site then
site_config = config.site
end
end
local raw_favicon = site_config.favicon or [[<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔥</text></svg>]]
local self = {
version = site_config.version or "",
title = site_config.title or "Document",
copyright = site_config.copyright or "&#169; The Copyright Holder",
favicon = "data:image/svg+xml," .. encode(raw_favicon),
header = header or [[
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="{{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; }
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; }
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: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 summary::-webkit-details-marker { display: none; }
details summary::before { content: "▸"; margin-right: 8px; transition: transform .15s ease; color: #68a6ff; }
details[open] summary::before { transform: rotate(90deg); }
summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: #9aa6b1; }
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; }
.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; }
ul, ol { margin: 14px 0; padding-left: 26px; }
.tl { display: grid; grid-template-columns: repeat(auto-fill, 200px); gap: 15px; list-style-type: none; padding: 0; margin: 0; justify-content: start; }
ul.tl li { padding: 10px; width: fit-content; }
li { margin: 6px 0; }
code { padding: 3px 7px; border-radius: 3px; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: .9em; color: #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; }
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; }
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); }
.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; }
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); }
.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; }
.left { text-align: left; float: left; }
.right { text-align: right; float: right; }
</style>
</head>
<body>
<div class="container">
]],
footer = footer or [[
<footer class="footer">
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
<a href="https://www.lua.org/" target="_blank">Lua Powered</a>
<a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a>
<p>{{COPYRIGHT}}</p>
</footer>
</div>
</body>
</html>
]],
parts = {}
}
return setmetatable(self, M)
end
function M:custom(str)
table.insert(self.parts, str)
return self
end
for name, func in pairs(std) do
if type(func) == "function" then
M[name] = function(self, ...)
local result = func(...)
table.insert(self.parts, result)
return self
end
end
end
function M:build()
local header = self.header
local safe_title = self.title or "Document"
local safe_favicon = self.favicon:gsub("%%", "%%%%")
header = header:gsub("{{TITLE}}", safe_title)
header = header:gsub("{{FAVICON}}", safe_favicon)
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
end
M.__tostring = function(self)
return self:build()
end
return M