106 lines
3.0 KiB
JavaScript
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}`);
|