People who want to set s3d up themselves, or with an AI assistant, had only the feature tour. docs/ explains how Sia, the indexer, s3d, users, keys and drives fit together; every Pithos screen; the web console and desktop app; every s3d command, s3d.yml, the admin API and where files live; AWS CLI and rclone setup; common tasks, troubleshooting and limits; and a brief for AI assistants with the rule that recovery phrases and secrets are never typed into a chat. The Markdown is the source (published beside the page for assistants, with llms.txt pointing to it); docs/build-docs.mjs renders the HTML.
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 /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`);
|