fes.lua (8653B)
1 local std = require("lib.std") 2 local symbol = require("lib.symbol") 3 4 local M = {} 5 M.__index = M 6 7 function M.fes(header, footer) 8 local config = {} 9 local site_config = {} 10 local fes_mod = package.loaded.fes 11 if fes_mod and fes_mod.config then 12 config = fes_mod.config 13 if config.site then 14 site_config = config.site 15 end 16 end 17 18 if site_config.favicon then 19 site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">' 20 end 21 22 local self = { 23 version = site_config.version, 24 title = site_config.title, 25 copyright = site_config.copyright, 26 favicon = site_config.favicon, 27 header = header or [[ 28 <!DOCTYPE html> 29 <html lang="en"> 30 <head> 31 <meta charset="UTF-8"> 32 <meta name="viewport" content="width=device-width,initial-scale=1.0"> 33 {{FAVICON}} 34 <title>{{TITLE}}</title> 35 <style> 36 :root { 37 --bg: #f5f5f5; 38 --text: #111827; 39 --muted: #6b7280; 40 --link: #1a0dab; 41 --accent: #68a6ff; 42 --highlight: #004d99; 43 --note-bg: #ffffff; 44 --panel-bg: #ffffff; 45 --border: rgba(0,0,0,.1); 46 --table-head: #f3f4f6; 47 --code-color: #004d99; 48 --blockquote-border: #1a73e8; 49 --banner-bg: #ffffff; 50 --footer-bg: #ffffff; 51 --shadow: rgba(0,0,0,.08); 52 } 53 54 @media (prefers-color-scheme: dark) { 55 :root { 56 --bg: #0f1113; 57 --text: #e6eef3; 58 --muted: #9aa6b1; 59 --link: #68a6ff; 60 --accent: #68a6ff; 61 --highlight: #cde7ff; 62 --note-bg: #1a1c20; 63 --panel-bg: #1a1c20; 64 --border: rgba(255,255,255,.06); 65 --table-head: #1a1c20; 66 --code-color: #cde7ff; 67 --blockquote-border: #68a6ff; 68 --banner-bg: #1a1c20; 69 --footer-bg: #1a1c20; 70 --shadow: rgba(0,0,0,.4); 71 } 72 } 73 74 html, body { 75 min-height: 100%; 76 margin: 0; 77 padding: 0; 78 background: var(--bg); 79 color: var(--text); 80 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 81 line-height: 1.5; 82 -webkit-font-smoothing: antialiased; 83 -moz-osx-font-smoothing: grayscale; 84 } 85 86 body { padding: 36px; } 87 88 .container { max-width: 830px; margin: 0 auto; } 89 90 .container > *:not(.banner) { margin: 28px 0; } 91 92 h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; } 93 94 h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; } 95 96 h2 { font-size: 32px; margin: 26px 0 14px; } 97 98 h3 { font-size: 26px; margin: 22px 0 12px; } 99 100 h4 { font-size: 20px; margin: 18px 0 10px; } 101 102 h5 { font-size: 16px; margin: 16px 0 8px; } 103 104 h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); } 105 106 p { margin: 14px 0; } 107 108 a { color: var(--link); text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; } 109 110 .hidden { color: var(--text); text-decoration: none; } 111 112 a:hover { text-decoration: underline; } 113 114 summary { cursor: pointer; } 115 116 details { 117 background: var(--panel-bg); 118 border: 1px solid var(--border); 119 border-radius: 4px; 120 padding: 14px 16px; 121 margin: 16px 0; 122 } 123 124 details summary { 125 list-style: none; 126 font-weight: 600; 127 color: var(--text); 128 display: flex; 129 align-items: center; 130 } 131 132 details summary::-webkit-details-marker { display: none; } 133 134 details summary::before { 135 content: "▸"; 136 margin-right: 8px; 137 transition: transform .15s ease; 138 color: var(--accent); 139 } 140 141 details[open] summary::before { transform: rotate(90deg); } 142 143 summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); } 144 145 details[open] summary::after { content: "Collapse"; } 146 147 details > *:not(summary) { margin-top: 12px; } 148 149 .note, pre, code { 150 background: var(--note-bg); 151 border: 1px solid var(--border); 152 } 153 154 .note { 155 padding: 20px; 156 border-radius: 4px; 157 background: var(--note-bg); 158 border: 1px solid var(--border); 159 margin: 28px 0; 160 color: var(--text); 161 } 162 163 .note strong { color: var(--text); } 164 165 .muted { color: var(--muted); } 166 167 .lead { font-size: 15px; margin-top: 8px; } 168 169 .callout { display: block; margin: 12px 0; } 170 171 .small { font-size: 13px; color: var(--muted); margin-top: 6px; } 172 173 .highlight { font-weight: 700; color: var(--highlight); } 174 175 ul, ol { margin: 14px 0; padding-left: 26px; } 176 177 .tl { 178 display: grid; 179 grid-template-columns: repeat(auto-fill, 200px); 180 gap: 15px; 181 list-style-type: none; 182 padding: 0; 183 margin: 0; 184 justify-content: start; 185 } 186 187 ul.tl li { padding: 10px; width: fit-content; } 188 189 li { margin: 6px 0; } 190 191 code { 192 padding: 3px 7px; 193 border-radius: 3px; 194 font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; 195 font-size: .9em; 196 color: var(--code-color); 197 } 198 199 pre { 200 padding: 20px; 201 border-radius: 4px; 202 margin: 14px 0; 203 overflow-x: auto; 204 font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; 205 font-size: 14px; 206 line-height: 1.6; 207 } 208 209 pre code { background: none; border: none; padding: 0; font-size: inherit; } 210 211 blockquote { 212 border-left: 3px solid var(--blockquote-border); 213 padding-left: 18px; 214 margin: 14px 0; 215 color: var(--text); 216 font-style: italic; 217 } 218 219 hr { border: 0; border-top: 1px solid rgba(0,0,0,.08); margin: 26px 0; } 220 221 @media (prefers-color-scheme: dark) { 222 hr { border-top-color: rgba(255,255,255,.1); } 223 } 224 225 img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; } 226 227 table { width: 100%; border-collapse: collapse; margin: 14px 0; } 228 229 th, td { 230 padding: 12px 16px; 231 text-align: left; 232 border-bottom: 1px solid var(--border); 233 } 234 235 th { 236 background: var(--table-head); 237 font-weight: 600; 238 color: var(--text); 239 } 240 241 tr:hover { background: rgba(0,0,0,0.02); } 242 243 @media (prefers-color-scheme: dark) { 244 tr:hover { background: rgba(255,255,255,0.02); } 245 } 246 247 .divider { margin: 26px 0; height: 1px; background: rgba(0,0,0,.08); } 248 249 @media (prefers-color-scheme: dark) { 250 .divider { background: rgba(255,255,255,.1); } 251 } 252 253 .section { margin-top: 36px; } 254 255 .links { margin: 12px 0; } 256 257 .links a { display: inline-block; margin: 0 14px 6px 0; color: var(--link); } 258 259 strong, b { font-weight: 600; color: var(--text); } 260 261 em, i { font-style: italic; } 262 263 .center { display: flex; justify-content: center; align-items: center; } 264 265 .banner { 266 width: 100%; 267 box-sizing: border-box; 268 text-align: center; 269 background: var(--banner-bg); 270 padding: 20px; 271 border: 1px solid var(--border); 272 border-bottom-right-radius: 8px; 273 border-bottom-left-radius: 8px; 274 color: var(--text); 275 margin: -36px 0 28px 0; 276 box-shadow: 0 0.2em 0.6em var(--shadow); 277 } 278 279 .nav { margin-left: auto; margin-right: auto; } 280 281 .nav a { color: var(--highlight); } 282 283 .footer { 284 background: var(--footer-bg); 285 padding: 20px 0; 286 border-top: 1px solid rgba(0,0,0,.08); 287 font-size: 14px; 288 color: var(--muted); 289 display: flex; 290 justify-content: center; 291 align-items: center; 292 gap: 24px; 293 margin-top: 28px !important; 294 margin-bottom: 0; 295 } 296 297 .left { text-align: left; float: left; } 298 299 .right { text-align: right; float: right; } 300 </style> 301 </head> 302 <body> 303 <div class="container"> 304 ]], 305 footer = footer or [[ 306 <footer class="footer"> 307 <a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a> 308 <a href="https://www.lua.org/" target="_blank">Lua Powered</a> 309 <a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a> 310 <p>{{COPYRIGHT}}</p> 311 </footer> 312 </div> 313 </body> 314 </html> 315 ]], 316 parts = {}, 317 } 318 319 return setmetatable(self, M) 320 end 321 322 function M:g(str) 323 table.insert(self.parts, str) 324 return self 325 end 326 327 function M:extend(name, tbl) 328 if type(name) ~= "string" then 329 error("First argument to extend must be a string (namespace name)") 330 end 331 if type(tbl) ~= "table" then 332 error("Second argument to extend must be a table of functions") 333 end 334 self[name] = {} 335 for k, v in pairs(tbl) do 336 if type(v) ~= "function" then 337 error("Extension values must be functions, got " .. type(v) .. " for key " .. k) 338 end 339 self[name][k] = function(...) 340 return v(self, ...) 341 end 342 end 343 return self 344 end 345 346 for name, func in pairs(std) do 347 if type(func) == "function" then 348 M[name] = function(self, ...) 349 local result = func(...) 350 table.insert(self.parts, result) 351 return self 352 end 353 end 354 end 355 356 function M:build() 357 local header = self.header 358 header = header:gsub("{{TITLE}}", self.title or "Document") 359 local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">') 360 header = header:gsub( 361 "{{FAVICON}}", 362 favicon_html 363 or 364 [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]] 365 ) 366 local footer = self.footer:gsub("{{COPYRIGHT}}", 367 self.copyright or symbol.legal.copyright .. "The Copyright Holder") 368 return header .. table.concat(self.parts, "\n") .. footer 369 end 370 371 M.__tostring = function(self) 372 return self:build() 373 end 374 375 return M