theseus-site/whitepaper/build/build.mjs
Hermes Dev 95f1ab55aa Whitepaper: rebuildable from its Markdown, with the workflow written down
The page was generated by a one-off script that no longer exists, so a text
change meant rebuilding the tooling first. build/build.mjs turns
whitepaper.md into index.html byte-for-byte as published; the page chrome and
the two diagrams are checked in beside it (the folder needs an exception from
the generic build/ ignore rule). README.md records the publishing steps for
both mirrors and the editorial decisions the text follows.
2026-11-01 18:01:58 +02:00

95 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
// Builds ../index.html (the theseus.x whitepaper page) from ../whitepaper.md.
//
// node build/build.mjs # write ../index.html
// node build/build.mjs --check # exit 1 if ../index.html is not what the build produces
//
// whitepaper.md is the Markdown export of the editable document (see ../README.md).
// The page chrome lives in template.html; the two diagrams are the figure-*.html
// fragments and are placed where the export left its "[embedded content: <name> · …]"
// marker. Markdown handled: #/## headings, paragraphs, "- " and "1. " lists, pipe
// tables, **bold**, `code`, [text](url), backslash escapes. Each paragraph is one line.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(here, "..");
const md = fs.readFileSync(path.join(root, "whitepaper.md"), "utf8").replace(/\r\n/g, "\n");
const template = fs.readFileSync(path.join(here, "template.html"), "utf8");
const figure = (name) => {
const f = path.join(here, `figure-${name}.html`);
if (!fs.existsSync(f)) throw new Error(`no figure fragment for "${name}" (expected ${f})`);
return fs.readFileSync(f, "utf8").replace(/\n$/, "");
};
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const unescapeMd = (s) => s.replace(/\\([\\`*_{}\[\]()#+\-.!|>~"'])/g, "$1");
// Inline Markdown → HTML. Code spans are protected first so nothing inside them is touched.
function inline(src) {
const codes = [];
let s = src.replace(/`([^`]+)`/g, (_, c) => { codes.push(`<code>${esc(c)}</code>`); return `\u0000${codes.length - 1}\u0000`; });
s = esc(unescapeMd(s));
s = s.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, u) => `<a href="${u}">${t}</a>`);
return s.replace(/\u0000(\d+)\u0000/g, (_, i) => codes[+i]);
}
const slug = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
const cells = (row) => row.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
const lines = md.split("\n");
let title = "", byline = "";
const toc = [], out = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) continue;
if (line.startsWith("# ")) { title = line.slice(2).trim(); continue; }
if (line.startsWith("## ")) {
const text = line.slice(3).trim(), id = slug(text);
toc.push(` <a href="#${id}">${inline(text)}</a>`);
out.push(`<h2 id="${id}">${inline(text)}</h2>`);
continue;
}
if (!toc.length && !byline) { byline = inline(line.trim()); continue; }
const embed = line.match(/^(?:\[|&#91;)embedded content:\s*([a-z0-9-]+)\s*(?:·.*)?(?:\]|\\\]|&#93;)\s*$/i);
if (embed) { out.push(figure(embed[1])); continue; }
if (/^[-*] /.test(line) || /^\d+\. /.test(line)) {
const ordered = /^\d+\. /.test(line);
const items = [];
while (i < lines.length && (ordered ? /^\d+\. / : /^[-*] /).test(lines[i])) items.push(lines[i++].replace(/^(?:[-*]|\d+\.) /, ""));
i--;
out.push(`<${ordered ? "ol" : "ul"}>${items.map((t) => `<li>${inline(t)}</li>`).join("")}</${ordered ? "ol" : "ul"}>`);
continue;
}
if (line.trimStart().startsWith("|")) {
const rows = [];
while (i < lines.length && lines[i].trimStart().startsWith("|")) rows.push(lines[i++]);
i--;
const head = cells(rows[0]), body = rows.slice(2).filter((r) => !/^\s*\|?\s*(:?-+:?\s*\|)+/.test(r) || true);
const tr = (cs, tag) => `<tr>${cs.map((c) => `<${tag}>${inline(c)}</${tag}>`).join("")}</tr>`;
out.push(`<table><thead>${tr(head, "th")}</thead><tbody>${rows.slice(2).map((r) => tr(cells(r), "td")).join("")}</tbody></table>`);
continue;
}
out.push(`<p>${inline(line.trim())}</p>`);
}
if (!title || !byline) throw new Error("whitepaper.md needs a '# title' line and a byline paragraph before the first '## ' heading");
// "Theseus Navigator Whitepaper" → last word in the accent colour, like the other theseus.x pages.
const titleHtml = esc(title).replace(/\s(\S+)$/, ' <span class="g">$1</span>');
const html = template
.replace("{{title}}", titleHtml)
.replace("{{byline}}", byline)
.replace("{{toc}}", toc.join("\n"))
.replace("{{article}}", out.join("\n"));
const target = path.join(root, "index.html");
if (process.argv.includes("--check")) {
const cur = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
if (cur !== html) { console.error("index.html differs from the build of whitepaper.md"); process.exit(1); }
console.log("index.html is up to date");
} else {
fs.writeFileSync(target, html);
console.log(`wrote ${path.relative(process.cwd(), target)} (${html.length} bytes, ${toc.length} sections)`);
}