#!/usr/bin/env node /** * Blog build script — converts markdown posts to static HTML. * * Usage: node build.js * Input: posts/*.md (markdown with YAML frontmatter) * Output: *.html (from _template.html + _index_template.html) * * Zero external dependencies — uses only Node.js built-ins. * Markdown conversion is intentionally minimal (handles the subset * our posts actually use). For richer formatting, swap in marked/markdown-it. */ const fs = require("fs"); const path = require("path"); const BLOG_DIR = __dirname; const POSTS_DIR = path.join(BLOG_DIR, "posts"); const TEMPLATE = fs.readFileSync(path.join(BLOG_DIR, "_template.html"), "utf8"); const INDEX_TEMPLATE = fs.readFileSync( path.join(BLOG_DIR, "_index_template.html"), "utf8" ); // ── Frontmatter parser ────────────────────────────────────────────── function parseFrontmatter(src) { const match = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); if (!match) throw new Error("Missing YAML frontmatter"); const meta = {}; for (const line of match[1].split("\n")) { const idx = line.indexOf(":"); if (idx === -1) continue; const key = line.slice(0, idx).trim(); let val = line.slice(idx + 1).trim(); // strip surrounding quotes if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } meta[key] = val; } return { meta, body: match[2] }; } // ── Minimal Markdown → HTML ───────────────────────────────────────── function md(src) { const lines = src.split("\n"); let html = ""; let inUl = false; let inCode = false; let codeLang = ""; let codeLines = []; function closeUl() { if (inUl) { html += "\n"; inUl = false; } } for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Fenced code blocks if (line.startsWith("```")) { if (!inCode) { closeUl(); inCode = true; codeLang = line.slice(3).trim(); codeLines = []; continue; } else { html += `
${codeLines.join("\n")}
\n`; inCode = false; continue; } } if (inCode) { codeLines.push(escapeHtml(line)); continue; } // Blank line if (line.trim() === "") { closeUl(); continue; } // Headings const hMatch = line.match(/^(#{1,6})\s+(.*)$/); if (hMatch) { closeUl(); const level = hMatch[1].length; html += `${inline(hMatch[2])}\n`; continue; } // Horizontal rule if (/^---+$/.test(line.trim())) { closeUl(); html += "
\n"; continue; } // Figure (![alt](src) on its own line) const figMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); if (figMatch) { closeUl(); html += `
\n ${escapeHtml(figMatch[1])}\n
${inline(figMatch[1])}
\n
\n`; continue; } // Unordered list const liMatch = line.match(/^[-*]\s+(.*)$/); if (liMatch) { if (!inUl) { html += "