The Share links card described the old week-long s3d links; public links at navigate.st/sia are now the feature that works for anyone. The web console no longer says "source release coming" because the code is public on the forge, and canonical URLs follow the mirror to silentmode.st/pithos.
171 lines
10 KiB
JavaScript
171 lines
10 KiB
JavaScript
// 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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 silentmode.st/pithos/ 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`);
|