// ============================================================================ // Project : localvm-research // File : web/lib/render.js // Purpose : HTML layout, markdown/code rendering, SVG chart for the platform // Author : Simon-Pierre Boucher // Contact : contact@spboucher.ai // Created : 2026-08-12 // Modified : 2026-08-12 // Platform : macOS / Apple Silicon (arm64) — Node.js // License : All rights reserved (research code) // ============================================================================ "use strict"; const { Marked } = require("marked"); const hljs = require("highlight.js"); function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function slugify(s) { return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 80); } /** * Render markdown → { html, toc }. A fresh Marked instance per call (a shared * global `marked.use()` would stack walkTokens handlers on every render). * Headings get stable anchor ids collected into a table of contents. */ function markdownToHtml(md, baseRel) { const toc = []; const seen = new Set(); const m = new Marked({ renderer: { code({ text, lang }) { const html = lang && hljs.getLanguage(lang) ? hljs.highlight(text, { language: lang }).value : esc(text); return `
${html}
`; }, heading({ tokens, depth }) { const html = this.parser.parseInline(tokens); const plain = html.replace(/<[^>]+>/g, ""); let id = slugify(plain) || "section"; while (seen.has(id)) id += "-x"; seen.add(id); if (depth >= 2 && depth <= 3) toc.push({ depth, id, text: plain }); return `${html}#\n`; }, }, walkTokens(token) { if (token.type === "image" && token.href && !/^(https?:|data:|\/)/.test(token.href)) { const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : ""; token.href = "/raw/" + (base ? base + "/" : "") + token.href; return; } if (token.type === "link" && token.href && !/^(https?:|mailto:|#|\/)/.test(token.href)) { const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : ""; const joined = (base ? base + "/" : "") + token.href; const norm = joined.split("/").reduce((acc, part) => { if (part === "..") acc.pop(); else if (part !== "." && part !== "") acc.push(part); return acc; }, []).join("/"); token.href = norm.endsWith(".md") ? "/doc/" + norm : "/file/" + norm; } }, }); return { html: m.parse(md), toc }; } /** Sticky table-of-contents sidebar for long documents. */ function tocHtml(toc) { if (!toc || toc.length < 3) return ""; const items = toc .map((t) => `
  • ${esc(t.text)}
  • `) .join(""); return ``; } /** Professional document header from front matter. */ function docHeader(data, rel) { const title = (data.document || rel).split("/").pop().replace(/[_-]/g, " "); const status = data.status || "draft"; const meta = [ data.created ? `created ${data.created}` : null, data.modified ? `updated ${data.modified}` : null, data.author || null, ].filter(Boolean).join(" · "); return `

    ${esc(title)}

    ${esc(status)}

    ${esc(data.document || rel)}${meta ? " · " + esc(meta) : ""}

    `; } /** Research-log timeline: entries already split by content.parseLogEntries. */ function logTimeline(entries, baseRel) { const items = entries .slice() .reverse() .map((e) => { const [when, ...rest] = e.title.split(" — "); const title = rest.join(" — ") || when; const body = markdownToHtml(e.body, baseRel).html; return `

    ${esc(title)}

    ${body}
    `; }) .join(""); return `
    ${items}
    `; } function highlightFile(text, filename) { const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase(); const langMap = { py: "python", sh: "bash", js: "javascript", css: "css", json: "json", toml: "ini", yaml: "yaml", yml: "yaml", cff: "yaml", md: "markdown", metal: "cpp", cpp: "cpp", h: "cpp", hpp: "cpp", swift: "swift", m: "objectivec", }; const lang = filename === "Makefile" ? "makefile" : langMap[ext]; if (lang && hljs.getLanguage(lang)) return hljs.highlight(text, { language: lang }).value; return esc(text); } const NAV = [ ["/", "Home"], ["/research", "Research"], ["/experiments", "Experiments"], ["/results", "Results"], ["/code", "Code"], ["/doc/research/bibliography.md", "Bibliography"], ["/publications", "Publications"], ["/comments", "Comments"], ["/about", "About"], ]; function layout({ title, active, body, buildInfo = {} }) { const nav = NAV.map( ([href, label]) => `${label}` ).join(""); const commit = buildInfo.commit ? buildInfo.commit.slice(0, 7) : "dev"; const synced = buildInfo.generated ? buildInfo.generated.slice(0, 10) : ""; return ` ${esc(title)} · localvm-research
    ${body}
    `; } /** * SSD throughput line chart (log-y), server-rendered SVG. * Form: change-of-magnitude across an ordered log2 x (block size), 3 series (QD). * Palette: validated categorical slots 1–3 (see dataviz reference palette); * relief for the aqua WARN is provided by direct labels at line ends. */ function ssdChartSvg(chart) { if (!chart) return ""; const SERIES_COLORS = { 1: "#2a78d6", 4: "#eb6834", 8: "#1baf7a" }; const W = 720, H = 340, ML = 64, MR = 96, MT = 18, MB = 44; const iw = W - ML - MR, ih = H - MT - MB; const blocks = [4096, 16384, 65536, 262144, 1048576]; const xPos = (b) => ML + (Math.log2(b) - 12) / (20 - 12) * iw; const yMin = Math.log10(50), yMax = Math.log10(20000); const yPos = (v) => MT + ih - ((Math.log10(v) - yMin) / (yMax - yMin)) * ih; const gridVals = [100, 1000, 10000]; let g = ""; for (const v of gridVals) { g += `` + `${v >= 1000 ? v / 1000 + " GB/s" : v + " MB/s"}`; } let xticks = ""; for (const b of blocks) { const lbl = b >= 1048576 ? "1 MiB" : b >= 1024 ? b / 1024 + " KiB" : b + " B"; xticks += `${lbl}`; } let lines = "", dots = "", labels = ""; const qds = Object.keys(chart.series).map(Number).sort((a, b) => a - b); for (const qd of qds) { const pts = chart.series[qd].filter((p) => blocks.includes(p.block)); if (!pts.length) continue; const c = SERIES_COLORS[qd] || "#2a78d6"; const d = pts.map((p, i) => `${i ? "L" : "M"}${xPos(p.block).toFixed(1)},${yPos(p.mbps).toFixed(1)}`).join(" "); lines += ``; for (const p of pts) { const val = p.mbps >= 1000 ? (p.mbps / 1000).toFixed(1) + " GB/s" : Math.round(p.mbps) + " MB/s"; dots += ``; } const last = pts[pts.length - 1]; labels += `QD ${qd}`; } return `
    ${g}${xticks}${lines}${dots}${labels} read block size (random, F_NOCACHE cold, log–log)
    Measured on this project's M5 Max (expH, run ${esc(chart.run)}, 3 repeats/cell, iostat-validated ceiling ≈ 13.1 GB/s). Lines: reader queue depth.
    `; } module.exports = { esc, markdownToHtml, tocHtml, docHeader, logTimeline, highlightFile, layout, ssdChartSvg };