const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const TEMPLATE = path.join(__dirname, 'main.html');
const FUNCDIR = path.join(__dirname, 'functions');
const OUTFILE = path.join(__dirname, 'index.html');
const FUNCTIONS_LIST_JSON_OUTFILE = 'functions-list.json';
function randomUuid() {
return crypto.randomBytes(16).toString('hex');
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
console.log('═══════════════════════════════════════════════');
console.log(' Lava Doc Site Builder - build-single-index.js ');
console.log('═══════════════════════════════════════════════\n');
console.log(`Reading HTML template from: ${TEMPLATE}`);
const template = fs.readFileSync(TEMPLATE, 'utf8');
console.log(`Scanning function/article docs in: ${FUNCDIR}`);
const files = fs.readdirSync(FUNCDIR).filter(f => f.endsWith('.json'));
console.log(`Found ${files.length} documentation files.\n`);
let docs = [];
let failedFiles = [];
let parseErrors = [];
for (const file of files) {
const fullPath = path.join(FUNCDIR, file);
try {
console.log(`Reading: ${file} ...`);
const raw = fs.readFileSync(fullPath, 'utf8');
const obj = JSON.parse(raw);
docs.push({ obj, file });
console.log(` ✔ Loaded "${file}" OK.`);
} catch (e) {
console.error(` ✖ Failed to read/parse "${file}": ${e.message}`);
failedFiles.push(file);
parseErrors.push({ file, error: e });
}
}
console.log('\nSorting documentation blocks...');
docs.sort((a, b) => {
const aId = a.obj.heading?.title?.toLowerCase() || '';
const bId = b.obj.heading?.title?.toLowerCase() || '';
return aId.localeCompare(bId);
});
const articleDocs = [];
const functionDocs = [];
for (const {obj, file} of docs) {
const badges = (obj.heading && Array.isArray(obj.heading.badges)) ? obj.heading.badges : [];
if (badges.includes('ARTICLE')) {
articleDocs.push({obj, file});
} else {
functionDocs.push({obj, file});
}
}
const componentSet = new Set();
const groupSet = new Set();
for (const {obj} of functionDocs) {
if (obj.dataComponent && obj.dataComponent.trim()) {
componentSet.add(obj.dataComponent.trim());
}
if (obj.group && obj.group.trim()) {
groupSet.add(obj.group.trim());
}
}
componentSet.add('core');
const groupUuidMap = {};
for (const g of groupSet) {
if (!groupUuidMap[g]) {
groupUuidMap[g] = randomUuid();
}
}
console.log('Generating sidebar navigation...');
let navHtml = `
<div class="mb-1rem">
<label for="component-filter-select" class="bold">Component:</label>
<select id="component-filter-select" class="full-width mt-03">
<option value="">All Components</option>
`;
const sortedComps = [...componentSet].sort();
for (const c of sortedComps) {
navHtml += ` <option value="${escapeHtml(c)}">${escapeHtml(c)}</option>\n`;
}
navHtml += ` </select>
</div>
<div class="mb-1rem">
<label for="group-filter-select" class="bold">Group:</label>
<select id="group-filter-select" class="full-width mt-03">
<option value="">All Groups</option>
`;
const sortedGroups = [...groupSet].sort();
for (const g of sortedGroups) {
navHtml += ` <option value="${escapeHtml(groupUuidMap[g])}">${escapeHtml(g)}</option>\n`;
}
navHtml += ` </select>
</div>
<div class="search-box">
<input type="text" placeholder="Filter" id="filter-input" class="w-80" />
</div>
<div class="function-list-container">
<ul class="function-list">
`;
navHtml += ` <li data-component="core" data-groupUuid="">
<a href="#core:extend" data-component="core">core:extend</a>
</li>\n`;
for (const {obj} of functionDocs) {
const comp = (obj.dataComponent || '').trim();
const t = obj.heading?.title || '';
const finalId = `${comp}:${t}`;
let grpUuid = '';
if (obj.group && obj.group.trim()) {
grpUuid = groupUuidMap[obj.group.trim()];
}
const linkText = `${comp}:${t}`;
navHtml += ` <li data-component="${escapeHtml(comp)}" data-groupUuid="${escapeHtml(grpUuid)}">
<a href="#${escapeHtml(finalId)}" data-component="${escapeHtml(comp)}">
${escapeHtml(linkText)}
</a>
</li>\n`;
}
navHtml += ` </ul>
</div>
`;
console.log('Rendering documentation blocks...');
let renderedIds = [];
let renderFailures = [];
function buildDocBlockHTML(obj, file, groupUuidMap) {
try {
const comp = obj.dataComponent || '';
const title = obj.heading?.title || '(No title)';
const id = comp && title ? `${comp}:${title}` : 'unknown';
let docGroupUuid = '';
if (obj.group && obj.group.trim()) {
if (!groupUuidMap[obj.group]) {
groupUuidMap[obj.group] = randomUuid();
}
docGroupUuid = groupUuidMap[obj.group];
}
const badges = Array.isArray(obj.heading?.badges) ? obj.heading.badges : [];
const badgeHtml = badges.map(b => `<span class="category-badge">${escapeHtml(b)}</span>`).join(' ');
const compBadge = comp ? `<span class="component-badge">:${escapeHtml(comp)}</span>` : '';
const h1 = `<h1>${escapeHtml(title)} ${compBadge} ${badgeHtml}</h1>`;
let synopsisHtml = '';
if (obj.synopsis && obj.synopsis.trim()) {
synopsisHtml = `
<div class="description-block bg-orange">
${escapeHtml(obj.synopsis)}
</div>`;
}
let codeBlocksHtml = '';
if (Array.isArray(obj.codeBlocks)) {
for (const block of obj.codeBlocks) {
codeBlocksHtml += `<div class="code-block"><pre><code>${escapeHtml(block.replace(/\\n/g, '\n'))}</code></pre></div>\n`;
}
}
let notesHtml = '';
if (Array.isArray(obj.notes) && obj.notes.length > 0) {
const lines = obj.notes.map(line => `<p>${escapeHtml(line)}</p>`).join('');
notesHtml = `<div class="notes-block">${lines}</div>`;
}
renderedIds.push(id);
console.log(`✔ Rendered: ${id}`);
return `<!-- rendered: ${id} -->\n<div class="doc-block" id="${escapeHtml(id)}" data-component="${escapeHtml(comp)}" data-groupUuid="${escapeHtml(docGroupUuid)}">
${h1}
${synopsisHtml}
${codeBlocksHtml}
${notesHtml}
</div>`;
} catch (e) {
const id = obj && obj.heading && obj.heading.title ? `${obj.dataComponent || ''}:${obj.heading.title}` : file;
renderFailures.push(id);
console.error(`✖ Failed to render doc block for "${id}": ${e.message}`);
return `<!-- Failed to render: ${id} -->`;
}
}
let docBlocksHtml = '';
for (const {obj, file} of articleDocs) {
docBlocksHtml += buildDocBlockHTML(obj, file, {});
}
for (const {obj, file} of functionDocs) {
docBlocksHtml += buildDocBlockHTML(obj, file, groupUuidMap);
}
let summaryHtml = `
<!-- Build summary -->
<div style="font-size:0.9em;color:#666;margin:2em 0 0 0;">
<b>Rendered ${renderedIds.length} docs:</b> ${renderedIds.join(', ') || '(none)'}<br/>
${failedFiles.length + renderFailures.length
? `<b>Failed files:</b> ${(failedFiles.concat(renderFailures)).join(', ')}`
: 'All files rendered successfully.'}
</div>
`;
console.log('Inserting generated content into template...');
let result = template;
result = result.replace('<!--FUNCTION_NAV-->', navHtml);
result = result.replace('<!--DOC_BLOCKS-->', docBlocksHtml + summaryHtml);
console.log(`Writing output to: ${OUTFILE}`);
fs.writeFileSync(OUTFILE, result, 'utf8');
console.log('✔ index.html generated successfully!\n');
const functionList = files.map(full => full.replace(/\\/g, '/'));
fs.writeFileSync(FUNCTIONS_LIST_JSON_OUTFILE, JSON.stringify(functionList, null, 2), 'utf8');
console.log(`Wrote '${FUNCTIONS_LIST_JSON_OUTFILE}' with an array of filenames only.`);
if (failedFiles.length || renderFailures.length) {
console.error(`There were ${failedFiles.length + renderFailures.length} file(s) with errors:`);
for (const fname of failedFiles) {
console.error(` - Failed to load: ${fname}`);
}
for (const rid of renderFailures) {
console.error(` - Failed to render: ${rid}`);
}
} else {
console.log('All documentation files loaded and rendered with no errors.');
}
console.log('\nDone. You can now open index.html in your browser.');