NODEDC_SITE/tools/render-home.mjs

106 lines
3.0 KiB
JavaScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const pagePath = join(repoRoot, "content", "pages", "home.json");
const templateRoot = join(repoRoot, "templates", "pages", "home");
const shellPath = join(templateRoot, "shell.html");
const blockRoot = join(templateRoot, "blocks");
const REGION_TOKEN = {
beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
cViz: "{{NODEDC_BLOCKS_CVIZ}}",
};
const page = JSON.parse(await readFile(pagePath, "utf8"));
const shell = await readFile(shellPath, "utf8");
const templateCache = new Map();
async function readTemplate(templateName) {
if (!templateName || templateName.includes("..") || templateName.includes("/")) {
throw new Error(`Unsafe template name: ${templateName}`);
}
if (!templateCache.has(templateName)) {
templateCache.set(templateName, await readFile(join(blockRoot, templateName), "utf8"));
}
return templateCache.get(templateName);
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function scopeDuplicateIds(html, scope) {
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
if (!ids.length) {
return html;
}
let scoped = html.replace(/\sid="([^"]+)"/g, (_match, id) => ` id="${scope}-${id}"`);
for (const id of ids) {
const escaped = escapeRegExp(id);
scoped = scoped.replace(
new RegExp(`\\s(data-target|for|aria-controls|aria-labelledby|aria-describedby)="${escaped}"`, "g"),
(_match, attr) => ` ${attr}="${scope}-${id}"`,
);
scoped = scoped.replace(new RegExp(`(href="[^"]*)#${escaped}(")`, "g"), `$1#${scope}-${id}$2`);
}
return scoped;
}
async function renderBlock(block, renderCounts) {
if (block.enabled === false) {
return "";
}
const templateName = block.template;
const renderCount = renderCounts.get(templateName) ?? 0;
renderCounts.set(templateName, renderCount + 1);
const html = await readTemplate(templateName);
if (renderCount === 0 && block.scopeIds !== true) {
return html;
}
return scopeDuplicateIds(html, block.id);
}
async function renderRegion(region) {
const renderCounts = new Map();
const blocks = [];
for (const block of region.blocks ?? []) {
blocks.push(await renderBlock(block, renderCounts));
}
return blocks.join("");
}
let output = shell;
for (const region of page.regions ?? []) {
const token = REGION_TOKEN[region.id];
if (!token) {
throw new Error(`Unknown region: ${region.id}`);
}
output = output.replace(token, await renderRegion(region));
}
for (const token of Object.values(REGION_TOKEN)) {
if (output.includes(token)) {
throw new Error(`Unrendered token left in shell: ${token}`);
}
}
const outputPath = join(repoRoot, page.output || "index.html");
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, output);
console.log(`Rendered ${page.slug} -> ${outputPath}`);