174 lines
5.5 KiB
JavaScript
174 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Generate output HTML from template + data for collecting-asteroids documents.
|
|
*
|
|
* Usage: node generate.mjs <docId>
|
|
* Example: node generate.mjs collecting-asteroids-3
|
|
*
|
|
* Reads: docs/<docId>.template.html
|
|
* Reads: docs/<docId>.data.json (optional)
|
|
* Writes: docs/<docId>.output.html
|
|
*
|
|
* data.json format:
|
|
* {
|
|
* pages: [{
|
|
* page: 1,
|
|
* asteroids: [{ index, left, top, rotate, scale, value, asteroid }],
|
|
* ships: [{ index, left, top, value }],
|
|
* baseScales: { "asteroid5": 0.35 }
|
|
* }]
|
|
* }
|
|
*/
|
|
|
|
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 <docId>');
|
|
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 HTML into pages by PAGE comment markers or by w-[210mm] divs
|
|
const pageMarker = /<!-- PAGE (\d+)/g;
|
|
const hasPageComments = pageMarker.test(html);
|
|
|
|
if (hasPageComments) {
|
|
// Split by page comments
|
|
return applyByPageComments(html, data.pages);
|
|
} else {
|
|
// Split by page divs
|
|
return applyByPageDivs(html, data.pages);
|
|
}
|
|
}
|
|
|
|
function applyByPageDivs(html, pages) {
|
|
// Find page boundaries: <div class="w-[210mm] h-[297mm]"
|
|
const pageRegex = /<div class="w-\[210mm\] h-\[297mm\]"/g;
|
|
const starts = [];
|
|
let match;
|
|
while ((match = pageRegex.exec(html)) !== null) {
|
|
starts.push(match.index);
|
|
}
|
|
|
|
if (starts.length === 0) return html;
|
|
|
|
// Process pages in reverse order to preserve indices
|
|
for (let i = pages.length - 1; i >= 0; i--) {
|
|
const pageData = 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 asteroid positions
|
|
if (pageData.asteroids) {
|
|
pageHtml = applyAsteroids(pageHtml, pageData.asteroids);
|
|
}
|
|
|
|
// Apply ship positions
|
|
if (pageData.ships) {
|
|
pageHtml = applyShips(pageHtml, pageData.ships);
|
|
}
|
|
|
|
html = html.slice(0, pageStart) + pageHtml + html.slice(pageEnd);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function applyByPageComments(html, pages) {
|
|
// Same logic but split by <!-- PAGE N -->
|
|
const parts = html.split(/(<!-- PAGE \d+)/);
|
|
const pageChunks = [];
|
|
let prelude = parts[0];
|
|
|
|
for (let i = 1; i < parts.length; i += 2) {
|
|
pageChunks.push(parts[i] + (parts[i + 1] || ''));
|
|
}
|
|
|
|
for (const pageData of pages) {
|
|
const pageIdx = (pageData.page || 1) - 1;
|
|
if (pageIdx >= pageChunks.length) continue;
|
|
|
|
if (pageData.asteroids) {
|
|
pageChunks[pageIdx] = applyAsteroids(pageChunks[pageIdx], pageData.asteroids);
|
|
}
|
|
if (pageData.ships) {
|
|
pageChunks[pageIdx] = applyShips(pageChunks[pageIdx], pageData.ships);
|
|
}
|
|
}
|
|
|
|
return prelude + pageChunks.join('');
|
|
}
|
|
|
|
function applyAsteroids(pageHtml, asteroids) {
|
|
// Match asteroid containers: <div class="absolute" style="left: Xmm; top: Ymm;">
|
|
// followed by pack3-asteroids image
|
|
const asteroidRegex = /(<div class="absolute" style="left: )-?\d+mm; top: -?\d+mm(;"><div class="relative w-\[88px\] h-\[88px\]"><img src="[^"]*pack3-asteroids\/[^"]*" class="w-full h-full object-contain" style="transform: rotate\()-?\d+deg(\) scale\()[\d.]+(\);" alt=""><div class="absolute inset-0 flex items-center justify-center"><span class="text-white font-extrabold text-2xl" style="text-shadow[^"]*">)\d+(<\/span>)/g;
|
|
|
|
let idx = 0;
|
|
return pageHtml.replace(asteroidRegex, (match, prefix, rotPrefix, scalePrefix, scaleSuffix, valueSuffix) => {
|
|
const ast = asteroids[idx++];
|
|
if (!ast) return match;
|
|
|
|
const left = ast.left.replace('mm', '');
|
|
const top = ast.top.replace('mm', '');
|
|
const rotate = ast.rotate.replace('deg', '');
|
|
const scale = ast.scale.toFixed(2);
|
|
const value = ast.value;
|
|
|
|
return `${prefix}${left}mm; top: ${top}mm${rotPrefix}${rotate}deg${scalePrefix}${scale}${scaleSuffix}${value}${valueSuffix}`;
|
|
});
|
|
}
|
|
|
|
function applyShips(pageHtml, ships) {
|
|
// Match ship capacity circles
|
|
const shipRegex = /(<div class="absolute w-12 h-12 rounded-full bg-indigo-600[^"]*" style="left: )[\d.]+%(; top: )[\d.]+(%[^>]*><span class="text-white font-extrabold text-xl">)\d+(<\/span>)/g;
|
|
|
|
let idx = 0;
|
|
return pageHtml.replace(shipRegex, (match, prefix, topPrefix, valueSuffix, spanClose) => {
|
|
const ship = ships[idx++];
|
|
if (!ship) return match;
|
|
|
|
const left = ship.left.replace('%', '');
|
|
const top = ship.top.replace('%', '');
|
|
|
|
return `${prefix}${left}%${topPrefix}${top}${valueSuffix}${ship.value}${spanClose}`;
|
|
});
|
|
}
|