#!/usr/bin/env node /** * Build periodic-table/index.html from periodic-table/table.yml. * * Strategy: the existing index.html is the template. Data sections are * marked with sentinel HTML comments (`` etc.). On * each run we replace the contents between the sentinels with freshly * emitted markup; everything else (CSS, render JS, prose) is preserved. * * On the FIRST run (before sentinels exist) we use a one-time bootstrap * that finds the current data definitions by their familiar `const ... =` * patterns and inserts the sentinel comments around them. * * Usage: node periodic-table/scripts/build-html.mjs */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import yaml from "js-yaml"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(__dirname, "..", ".."); const HTML_PATH = path.join(REPO, "periodic-table", "index.html"); const YAML_PATH = path.join(REPO, "periodic-table", "table.yml"); // ── 1. Load and validate ──────────────────────────────────────────────── const doc = yaml.load(fs.readFileSync(YAML_PATH, "utf8")); validate(doc); const KNOWN_SYMS = new Set(doc.elements.map((e) => e.sym)); // ── 2. Render the data sections from YAML ─────────────────────────────── const renderedBlocks = renderBlocksJs(doc.blocks, doc.rows); const renderedElements = renderElementsJs(doc.elements); const renderedCompounds = renderCompoundsHtml(doc.compounds, KNOWN_SYMS); // ── 3. Read the current HTML, bootstrap sentinels if needed, patch ────── let html = fs.readFileSync(HTML_PATH, "utf8"); html = bootstrapSentinels(html); html = replaceBetween(html, "@gen:blocks", renderedBlocks); html = replaceBetween(html, "@gen:elements", renderedElements); html = replaceBetween(html, "@gen:compounds", renderedCompounds); fs.writeFileSync(HTML_PATH, html); console.log(`Wrote ${HTML_PATH}`); console.log(` ${doc.elements.length} elements`); console.log(` ${doc.compounds.reduce((n, s) => n + s.items.length, 0)} compounds across ${doc.compounds.length} sections`); console.log(` ${(doc.known_collisions || []).length} documented symbol collisions`); // ════════════════════════════════════════════════════════════════════════ // Validation // ════════════════════════════════════════════════════════════════════════ function validate(doc) { if (!doc || typeof doc !== "object") throw new Error("table.yml is empty or malformed"); if (!Array.isArray(doc.elements) || doc.elements.length === 0) throw new Error("No elements defined"); const issues = []; const knownSyms = new Set(doc.elements.map((e) => e.sym)); const cellSeen = new Map(); for (const e of doc.elements) { const k = `${e.row},${e.col}`; if (cellSeen.has(k)) { issues.push(`Cell collision at (${k}): #${cellSeen.get(k)} and #${e.id}`); } else { cellSeen.set(k, e.id); } } for (const e of doc.elements) { for (const b of e.bonds || []) { if (!knownSyms.has(b)) issues.push(`Element #${e.id} ${e.sym}: unresolved bond '${b}'`); } } const symCount = {}; for (const e of doc.elements) symCount[e.sym] = (symCount[e.sym] || 0) + 1; const declared = new Set((doc.known_collisions || []).map((c) => c.sym)); for (const [sym, count] of Object.entries(symCount)) { if (count > 1 && !declared.has(sym)) { issues.push(`Undocumented symbol collision: ${sym} appears ${count} times — add to known_collisions`); } } for (const section of doc.compounds || []) { for (const item of section.items || []) { const refs = extractFormulaSymbols(item.formula); for (const ref of refs) { if (!knownSyms.has(ref)) { issues.push(`Compound "${item.name}" references unknown symbol '${ref}': ${item.formula}`); } } } } if (issues.length > 0) { console.error("VALIDATION FAILED:"); issues.forEach((i) => console.error(" " + i)); process.exit(1); } } function extractFormulaSymbols(formula) { const cleaned = formula.replace(/_[A-Za-z]+/g, ""); const re = /(? jsStr(r.name)).join(",")}];`; return out; } function renderElementsJs(elements) { let out = "// [num, sym, name, block, row, col, year, desc, bonds[], whyHere]\n"; out += "const elements = [\n"; const byRow = {}; for (const e of elements) (byRow[e.row] ||= []).push(e); const rowComments = { 1: "// Row 0: Data (The Raw Material)", 2: "// Row 1: Math (The Theoretical Bedrock)", 3: "// Row 2: Algorithms (The Operations)", 4: "// Row 3: Architecture (The Topologies)", 5: "// Row 4: Optimization (The Physics of Efficiency)", 6: "// Row 5: Runtime (Software Execution Primitives)", 7: "// Row 6: Hardware (Silicon Primitives)", 8: "// Row 7: Production (Fleet Primitives)", }; const rowKeys = Object.keys(byRow).map(Number).sort((a, b) => a - b); for (const row of rowKeys) { out += ` ${rowComments[row] || `// Row ${row}`}\n`; for (const e of byRow[row]) { const yearLit = e.year === null || e.year === undefined ? `'—'` : jsStr(e.year); const bondsLit = `[${(e.bonds || []).map(jsStr).join(",")}]`; out += ` [${e.id},${jsStr(e.sym)},${jsStr(e.name)},${jsStr(e.block)},${e.row},${e.col},${yearLit},${jsStr(e.desc)},${bondsLit},${jsStr(e.why)}],\n`; } out += "\n"; } // Drop the trailing blank line and trailing comma to keep diffs minimal. out = out.replace(/,\n\n$/, "\n"); out += "];"; return out; } function renderCompoundsHtml(compounds, knownSyms) { let out = ""; for (const section of compounds) { out += `\n