#!/usr/bin/env node /** * Generate output HTML from template + data for space-route documents. * * Usage: node generate.mjs * Example: node generate.mjs space-route-1 * * Reads: docs/.template.html * Reads: docs/.data.json (optional) * Writes: docs/.output.html * * data.json format (from space-route editor): * { * pages: [{ * page: 1, * objects: [{ nodeId, type, src, left, top, w, h, rotate, flipH }], * nodes: [{ nodeId, left, top }] * }] * } */ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { postGenerate } from '../../../src/scripts/post-generate.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const docsDir = join(__dirname, '..', 'docs'); const docId = process.argv[2]; if (!docId) { console.error('Usage: node generate.mjs '); process.exit(1); } const templatePath = join(docsDir, `${docId}.template.html`); const dataPath = join(docsDir, `${docId}.data.json`); const outputPath = join(docsDir, `${docId}.output.html`); if (!existsSync(templatePath)) { console.error(`Template not found: ${templatePath}`); process.exit(1); } let html = readFileSync(templatePath, 'utf-8'); if (existsSync(dataPath)) { const data = JSON.parse(readFileSync(dataPath, 'utf-8')); html = applyData(html, data); console.log(`Applied data from ${data.pages?.length || 0} pages`); } writeFileSync(outputPath, html); console.log(`Generated: ${outputPath}`); await postGenerate(outputPath); function applyData(html, data) { if (!data.pages) return html; // Split by page divs const pageRegex = /
= 0; i--) { const pageData = data.pages[i]; const pageNum = pageData.page || (i + 1); const pageIdx = pageNum - 1; if (pageIdx >= starts.length) continue; const pageStart = starts[pageIdx]; const pageEnd = pageIdx + 1 < starts.length ? starts[pageIdx + 1] : html.length; let pageHtml = html.slice(pageStart, pageEnd); // Apply object positions if (pageData.objects) { for (const obj of pageData.objects) { pageHtml = applyObject(pageHtml, obj); } } // Apply node positions and update edges if (pageData.nodes) { for (const node of pageData.nodes) { pageHtml = applyNode(pageHtml, node); } } html = html.slice(0, pageStart) + pageHtml + html.slice(pageEnd); } return html; } function applyObject(pageHtml, obj) { // Match: ]*data-node-id="${nodeId}"[^>]*style=")([^"]*)(")` ); return pageHtml.replace(regex, (match, prefix, oldStyle, suffix) => { const parts = []; parts.push(`left: ${obj.left}`); parts.push(`top: ${obj.top}`); if (obj.w) parts.push(`width: ${obj.w}`); if (obj.h) parts.push(`height: ${obj.h}`); const transforms = []; if (obj.rotate) transforms.push(`rotate(${obj.rotate}deg)`); if (obj.flipH) transforms.push('scaleX(-1)'); if (transforms.length) parts.push(`transform: ${transforms.join(' ')}`); // Preserve margin if present in old style const marginMatch = oldStyle.match(/margin[^;]+;/); if (marginMatch) parts.push(marginMatch[0].replace(/;$/, '')); return prefix + parts.join('; ') + ';' + suffix; }); } function applyNode(pageHtml, node) { // Update node div position const nodeRegex = new RegExp( `(]*data-node-id="${node.nodeId}"[^>]*style=")([^"]*)(")` ); pageHtml = pageHtml.replace(nodeRegex, (match, prefix, oldStyle, suffix) => { const newStyle = oldStyle .replace(/left:\s*[^;]+/, `left: ${node.left}`) .replace(/top:\s*[^;]+/, `top: ${node.top}`); return prefix + newStyle + suffix; }); // Update edges connected to this node const leftMm = parseFloat(node.left); const topMm = parseFloat(node.top); // Edges where this node is first: ]*data-edge="${node.nodeId}-\\d+"[^>]*)(x1=")[^"]*("\\s*y1=")[^"]*(")`,'g' ); pageHtml = pageHtml.replace(edgeRegex1, (match, prefix, x1p, y1p, suffix) => { return `${prefix}${x1p}${leftMm}mm${y1p}${topMm}mm${suffix}`; }); // Edges where this node is second: ]*data-edge="\\d+-${node.nodeId}"[^>]*)(x2=")[^"]*("\\s*y2=")[^"]*(")`,'g' ); pageHtml = pageHtml.replace(edgeRegex2, (match, prefix, x2p, y2p, suffix) => { return `${prefix}${x2p}${leftMm}mm${y2p}${topMm}mm${suffix}`; }); return pageHtml; }