pithos-site/docs/build-docs.mjs

172 lines
10 KiB
JavaScript
Raw Normal View History

// Builds docs/index.html from docs/pithos-docs.md (the Markdown stays the
// source of truth and is published beside it for AI assistants).
// node docs/build-docs.mjs
// Handles the Markdown this page uses: headings, paragraphs, lists (one
// nested level), fenced code, tables, inline code, bold, italics and links.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dir = path.dirname(fileURLToPath(import.meta.url));
const md = fs.readFileSync(path.join(dir, 'pithos-docs.md'), 'utf8').replace(/\r\n/g, '\n');
const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const slug = (s) => s.toLowerCase().replace(/<[^>]+>/g, '').replace(/[`*]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
function inline(s) {
const codes = [];
s = s.replace(/`([^`]+)`/g, (_, c) => { codes.push(`<code>${esc(c)}</code>`); return `\u0000${codes.length - 1}\u0000`; });
s = esc(s)
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>')
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2">$1</a>');
return s.replace(/\u0000(\d+)\u0000/g, (_, i) => codes[+i]);
}
const lines = md.split('\n');
const out = [];
const toc = [];
let i = 0;
let title = 'Pithos documentation';
while (i < lines.length) {
const line = lines[i];
if (!line.trim()) { i++; continue; }
const fence = /^```(\w*)/.exec(line);
if (fence) {
const buf = [];
i++;
while (i < lines.length && !lines[i].startsWith('```')) buf.push(lines[i++]);
i++;
out.push(`<pre class="code" data-lang="${fence[1]}"><button class="copy" type="button" aria-label="Copy">Copy</button><code>${esc(buf.join('\n'))}</code></pre>`);
continue;
}
const hd = /^(#{1,3}) (.+)$/.exec(line);
if (hd) {
const level = hd[1].length;
const text = hd[2];
if (level === 1) { title = text; i++; continue; }
const id = slug(text);
toc.push({ level, id, text: inline(text) });
out.push(`<h${level} id="${id}"><a class="anchor" href="#${id}" aria-hidden="true">#</a>${inline(text)}</h${level}>`);
i++;
continue;
}
if (line.startsWith('|')) {
const rows = [];
while (i < lines.length && lines[i].startsWith('|')) rows.push(lines[i++]);
const cells = (r) => r.replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
const head = cells(rows[0]);
const body = rows.slice(2).map(cells);
out.push(`<div class="table"><table><thead><tr>${head.map((c) => `<th>${inline(c)}</th>`).join('')}</tr></thead><tbody>${body.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`);
continue;
}
const li = /^(\s*)([-*]|\d+\.) (.+)$/.exec(line);
if (li) {
// Collect the list, with one nested level (3+ spaces of indent).
const ordered = /\d/.test(li[2]);
const items = [];
while (i < lines.length) {
const m = /^(\s*)([-*]|\d+\.) (.+)$/.exec(lines[i]);
if (!m) { if (lines[i].startsWith(' ') && items.length) { items[items.length - 1].text += ' ' + lines[i].trim(); i++; continue; } break; }
if (m[1].length >= 3 && items.length) (items[items.length - 1].sub ||= { ordered: /\d/.test(m[2]), items: [] }).items.push(m[3]);
else items.push({ text: m[3] });
i++;
}
const tag = ordered ? 'ol' : 'ul';
out.push(`<${tag}>${items.map((it) => `<li>${inline(it.text)}${it.sub ? `<${it.sub.ordered ? 'ol' : 'ul'}>${it.sub.items.map((s) => `<li>${inline(s)}</li>`).join('')}</${it.sub.ordered ? 'ol' : 'ul'}>` : ''}</li>`).join('')}</${tag}>`);
continue;
}
const para = [];
while (i < lines.length && lines[i].trim() && !/^(#{1,3} |```|\||\s*([-*]|\d+\.) )/.test(lines[i])) para.push(lines[i++].trim());
out.push(`<p>${inline(para.join(' '))}</p>`);
}
const tocHtml = toc.filter((t) => t.level === 2).map((t) => `<a href="#${t.id}">${t.text}</a>`).join('');
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>/* /docs and /docs/ (and the /pithos-sia/ mirror) resolve links the same way */ if (!location.pathname.endsWith("/")) document.write('<base href="' + location.pathname + '/">');</script>
<title>${esc(title)} — pithos.sia</title>
<meta name="description" content="Everything about Pithos and s3d, the Sia S3 gateway: the Pithos screens, the s3d command line, configuration, S3 clients, troubleshooting, and a brief for AI assistants.">
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">
<link rel="alternate" type="text/markdown" href="pithos-docs.md" title="Markdown version for AI assistants">
<style>
:root{ --bg:#0b100e; --panel:#121a17; --panel2:#17211d; --line:rgba(255,255,255,.08); --ink:#e6ece9; --mut:#8e9c96; --acc:#3ddc97; }
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:70px}
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#14271f,var(--bg));color:var(--ink);font:16px/1.7 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;padding-top:52px}
a{color:var(--acc)}
code{font-family:ui-monospace,"Cascadia Mono","SF Mono",Consolas,monospace;font-size:.86em;background:var(--panel2);padding:.12em .4em;border-radius:5px;overflow-wrap:anywhere}
.topnav{position:fixed;top:0;left:0;right:0;z-index:20;display:flex;gap:4px;align-items:center;padding:10px 1.2rem;background:rgba(11,16,14,.9);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:12px;font-size:15px;display:flex;align-items:center;gap:8px}
.topnav .brand img{width:20px;height:20px}
.topnav .brand .g{color:var(--acc)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
.topnav a:hover{color:var(--ink);background:var(--panel)}
.topnav a.on{color:var(--acc)}
.topnav a.md{margin-left:auto}
.layout{max-width:1180px;margin:0 auto;padding:2rem 1.2rem 5rem;display:grid;grid-template-columns:230px minmax(0,1fr);gap:2.4rem}
.toc{position:sticky;top:72px;align-self:start;display:flex;flex-direction:column;gap:2px;font-size:14px;max-height:calc(100vh - 90px);overflow:auto}
.toc .t{color:var(--mut);font-size:12px;text-transform:uppercase;letter-spacing:.07em;margin:0 0 6px 10px}
.toc a{color:var(--mut);text-decoration:none;padding:5px 10px;border-radius:7px}
.toc a:hover{color:var(--ink);background:var(--panel)}
article{min-width:0}
h1{font-size:clamp(1.9rem,4.6vw,2.5rem);margin:0 0 .4rem;letter-spacing:-.4px}
h2{font-size:1.45rem;margin:2.6rem 0 .8rem;padding-top:1.2rem;border-top:1px solid var(--line)}
h3{font-size:1.1rem;margin:1.8rem 0 .5rem}
h2 .anchor,h3 .anchor{color:var(--mut);text-decoration:none;margin-left:-1.1em;padding-right:.3em;opacity:0}
h2:hover .anchor,h3:hover .anchor{opacity:.6}
p,li{color:#cfd8d4}
strong{color:var(--ink)}
.lede{color:var(--mut);font-size:1.05rem}
.table{overflow-x:auto;margin:1rem 0;border:1px solid var(--line);border-radius:10px}
table{border-collapse:collapse;width:100%;font-size:14.5px}
th,td{text-align:left;vertical-align:top;padding:9px 12px;border-bottom:1px solid var(--line)}
th{background:var(--panel);color:var(--mut);font-weight:600}
tr:last-child td{border-bottom:none}
pre.code{position:relative;background:#0f1613;border:1px solid var(--line);border-radius:10px;padding:14px 16px;overflow:auto;margin:1rem 0}
pre.code code{background:none;padding:0;font-size:13.5px;line-height:1.6;overflow-wrap:normal;white-space:pre}
pre.code .copy{position:absolute;top:8px;right:8px;font:12px system-ui;color:var(--mut);background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:3px 9px;cursor:pointer;opacity:.75}
pre.code .copy:hover{opacity:1;color:var(--ink)}
.agent{background:var(--panel);border:1px solid rgba(61,220,151,.25);border-radius:12px;padding:14px 18px;margin:1.2rem 0}
footer{color:var(--mut);font-size:14px;border-top:1px solid var(--line);margin-top:3rem;padding-top:1.2rem}
@media (max-width:860px){
.layout{grid-template-columns:1fr;gap:0;padding-top:1.2rem}
.toc{position:static;max-height:none;flex-direction:row;flex-wrap:wrap;gap:4px;margin-bottom:1rem}
.toc .t{width:100%;margin-left:4px}
.toc a{background:var(--panel);font-size:13px}
.topnav a:not(.brand):not(.md){display:none}
}
</style>
</head>
<body>
<nav class="topnav">
<a class="brand" href="../"><img src="../assets/favicon.svg" alt=""><span>pithos<span class="g">.sia</span></span></a>
<a href="../#features">Features</a>
<a href="../#get">Get Pithos</a>
<a class="on" href="./">Docs</a>
<a class="md" href="pithos-docs.md" title="The same page as plain Markdown, for AI assistants">Markdown ↓</a>
</nav>
<div class="layout">
<nav class="toc" aria-label="On this page"><div class="t">On this page</div>${tocHtml}</nav>
<article>
<h1>${esc(title)}</h1>
<div class="agent"><strong>Using an AI assistant?</strong> Give it this page or the <a href="pithos-docs.md">Markdown version</a>, then describe what you want to do. Type recovery phrases and secret keys only into s3d or Pithos on your own computer, never into a chat.</div>
${out.join('\n ')}
<footer>Pithos is open source: <a href="https://code.silentmode.st/silentmode/pithos">code.silentmode.st/silentmode/pithos</a>. s3d is by the Sia Foundation: <a href="https://github.com/SiaFoundation/s3d">github.com/SiaFoundation/s3d</a>.</footer>
</article>
</div>
<script>
// With the <base> above, '#section' links would reload the page; scroll instead.
document.addEventListener('click', (e) => { const a = e.target.closest('a[href^="#"]'); if (!a) return; const t = document.getElementById(a.getAttribute('href').slice(1)); if (!t) return; e.preventDefault(); t.scrollIntoView({ behavior: 'smooth' }); history.replaceState(null, '', location.pathname + a.getAttribute('href')); });
document.querySelectorAll('pre.code .copy').forEach((b) => b.addEventListener('click', async () => {
try { await navigator.clipboard.writeText(b.nextElementSibling.textContent); b.textContent = 'Copied'; setTimeout(() => { b.textContent = 'Copy'; }, 1500); } catch {}
}));
</script>
</body>
</html>
`;
fs.writeFileSync(path.join(dir, 'index.html'), html);
console.log(`docs/index.html: ${toc.length} headings, ${out.length} blocks`);