math-tasks/tasks/cargo-filling/scripts/generate.mjs

207 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate output HTML from template + data for cargo-filling documents.
*
* Usage: node generate.mjs <docId>
* Example: node generate.mjs cargo-filling-1
*
* Reads: docs/<docId>.template.html
* Reads: docs/<docId>.data.json (optional)
* Writes: docs/<docId>.output.html
*
* data.json format:
* {
* pages: [{
* page: 1,
* cards: [{
* index: 0,
* asteroidScale: 1.15,
* ship: { right: -10, top: -3, width: 85, height: 40 },
* innerAst: { left: 60, top: 70 }
* }]
* }]
* }
*
* Transforms:
* - asteroidScale: scale on .space-asteroid img, .remnant-shape .shape-container, .inner-ast
* - ship: inline right/top/width/height on .cargo-ship
* - innerAst: inline left/top on .inner-ast (composed with translate and optional scale)
*/
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 w-[210mm] h-[297mm] divs
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 to preserve indices
for (let i = data.pages.length - 1; i >= 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);
if (pageData.cards) {
pageHtml = applyCards(pageHtml, pageData.cards);
}
html = html.slice(0, pageStart) + pageHtml + html.slice(pageEnd);
}
return html;
}
function applyCards(pageHtml, cards) {
// Find card boundaries by <!-- Card comments (two formats: "Card 1:" and "Card:")
const cardRegex = /<!-- Card[\s:]/g;
const cardStarts = [];
let match;
while ((match = cardRegex.exec(pageHtml)) !== null) {
cardStarts.push(match.index);
}
if (cardStarts.length === 0) return pageHtml;
// Process cards in reverse to preserve indices
for (let i = cards.length - 1; i >= 0; i--) {
const cardData = cards[i];
const cardIdx = cardData.index;
if (cardIdx >= cardStarts.length) continue;
const cardStart = cardStarts[cardIdx];
const cardEnd = cardIdx + 1 < cardStarts.length
? cardStarts[cardIdx + 1]
: pageHtml.length;
let cardHtml = pageHtml.slice(cardStart, cardEnd);
const scale = cardData.asteroidScale;
const ship = cardData.ship;
const innerAst = cardData.innerAst;
const badge = cardData.badge;
// Apply asteroidScale to .space-asteroid img
if (scale != null && scale !== 1) {
// Match <div class="space-asteroid"> then find the <img inside
cardHtml = cardHtml.replace(
/(<div class="space-asteroid">[\s]*<img [^>]*?)(>)/,
function(full, before, close) {
// Remove any existing style, then add new one
const cleaned = before.replace(/ style="[^"]*"/, '');
return cleaned + ' style="transform: scale(' + scale + ')"' + close;
}
);
}
// Apply asteroidScale to .remnant-shape .shape-container
if (scale != null && scale !== 1) {
cardHtml = cardHtml.replace(
/<div class="shape-container">/,
'<div class="shape-container" style="transform: scale(' + scale + ')">'
);
}
// Apply ship position/size to .cargo-ship
if (ship) {
const shipRight = ship.right != null ? ship.right : -12.5;
const shipTop = ship.top != null ? ship.top : -5;
const shipWidth = ship.width != null ? ship.width : 90;
const shipHeight = ship.height != null ? ship.height : 42;
const shipStyle = 'right: ' + shipRight + 'mm; top: ' + shipTop + 'mm; width: ' + shipWidth + 'mm; height: ' + shipHeight + 'mm';
cardHtml = cardHtml.replace(
/(<div class="cargo-ship")([\s>])/,
function(full, tag, after) {
return tag + ' style="' + shipStyle + '"' + after;
}
);
}
// Apply badge position
if (badge) {
const badgeLeft = badge.left != null ? badge.left : 40;
const badgeTop = badge.top != null ? badge.top : 39;
const badgeStyle = 'left: ' + badgeLeft + '%; top: ' + badgeTop + '%';
cardHtml = cardHtml.replace(
/(<div class="badge-10")([\s>])/,
function(full, tag, after) {
return tag + ' style="' + badgeStyle + '"' + after;
}
);
}
// Apply innerAst position (and compose with asteroidScale if present)
if (innerAst || (scale != null && scale !== 1)) {
const innerLeft = innerAst && innerAst.left != null ? innerAst.left : 58;
const innerTop = innerAst && innerAst.top != null ? innerAst.top : 73;
let transformPart = 'translate(-50%, -50%)';
if (scale != null && scale !== 1) {
transformPart = 'translate(-50%, -50%) scale(' + scale + ')';
}
let innerStyle = 'left: ' + innerLeft + '%; top: ' + innerTop + '%; transform: ' + transformPart;
// Only apply if we actually have something to change
const needsInnerChange = (innerAst && (innerAst.left != null || innerAst.top != null)) || (scale != null && scale !== 1);
if (needsInnerChange) {
cardHtml = cardHtml.replace(
/(<div class="inner-ast")([\s>])/,
function(full, tag, after) {
return tag + ' style="' + innerStyle + '"' + after;
}
);
}
}
pageHtml = pageHtml.slice(0, cardStart) + cardHtml + pageHtml.slice(cardEnd);
}
return pageHtml;
}