// ============================================================================
// 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 ``;
}
/** 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 `
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 };