import { readdir, readFile, mkdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { Marked } from 'marked'; import puppeteer from 'puppeteer'; const SPECS_DIR = resolve('specs'); const DIST_DIR = resolve('dist'); const OUTPUT_FILE = join(DIST_DIR, 'house-spec.pdf'); async function loadSpecs(): Promise<{ filename: string; content: string }[]> { const files = await readdir(SPECS_DIR); const mdFiles = files.filter(f => f.endsWith('.md')).sort(); const specs: { filename: string; content: string }[] = []; for (const file of mdFiles) { const content = await readFile(join(SPECS_DIR, file), 'utf-8'); specs.push({ filename: file, content }); } return specs; } function buildHtml(bodyHtml: string): string { return `

House Specification

Baufritz Build — Clean Air System

maintainable.xyz

Generated: ${new Date().toISOString().split('T')[0]}

Version 1.0

${bodyHtml} `; } /** * Extract mermaid code blocks from markdown, replacing them with unique * placeholders. Returns the cleaned markdown and a map of placeholder → diagram. * This prevents `marked` from corrupting the mermaid syntax (e.g. turning * indented lines into
 blocks or mangling special characters).
 */
function extractMermaidBlocks(markdown: string): {
  markdown: string;
  diagrams: Map;
} {
  const diagrams = new Map();
  let index = 0;
  const cleaned = markdown.replace(
    /```mermaid\n([\s\S]*?)```/g,
    (_match, diagram: string) => {
      const id = `MERMAIDPLACEHOLDER${index++}END`;
      diagrams.set(id, diagram.trim());
      return id;
    },
  );
  return { markdown: cleaned, diagrams };
}

/**
 * Re-inject mermaid diagrams into the HTML, replacing placeholders
 * with 
blocks containing the raw diagram source. */ function injectMermaidBlocks(html: string, diagrams: Map): string { let result = html; for (const [id, diagram] of diagrams) { // marked may have wrapped the placeholder in a

tag const wrappedPattern = new RegExp(`

${id}

`, 'g'); const rawPattern = new RegExp(id, 'g'); const replacement = `
\n${diagram}\n
`; result = result.replace(wrappedPattern, replacement); result = result.replace(rawPattern, replacement); } return result; } async function main() { console.log('Loading specs...'); const specs = await loadSpecs(); console.log(`Found ${specs.length} spec files.`); // Combine specs into raw markdown first let rawCombined = specs .map((spec, i) => { const sectionClass = i === 0 ? 'spec-section first' : 'spec-section'; return `
\n\n${spec.content}\n\n
`; }) .join('\n\n'); // Extract all mermaid blocks from the combined markdown (single pass, unique IDs) const { markdown: combinedMarkdown, diagrams: allDiagrams } = extractMermaidBlocks(rawCombined); console.log(`Extracted ${allDiagrams.size} mermaid diagrams.`); // Parse markdown to HTML — mermaid blocks are now just placeholder strings const marked = new Marked(); let bodyHtml = await marked.parse(combinedMarkdown); // Re-inject raw mermaid diagrams into the HTML bodyHtml = injectMermaidBlocks(bodyHtml, allDiagrams); const fullHtml = buildHtml(bodyHtml); // Ensure dist directory exists await mkdir(DIST_DIR, { recursive: true }); // Write intermediate HTML for debugging const htmlPath = join(DIST_DIR, 'house-spec.html'); const { writeFile } = await import('node:fs/promises'); await writeFile(htmlPath, fullHtml, 'utf-8'); console.log(`HTML written to: ${htmlPath}`); // Launch puppeteer and render PDF console.log('Launching browser...'); const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); // Set content and wait for mermaid to render await page.setContent(fullHtml, { waitUntil: 'networkidle0' }); // Wait for mermaid diagrams to render await page.evaluate(async () => { // Check if mermaid is loaded if (typeof (window as any).mermaid !== 'undefined') { await (window as any).mermaid.run(); } }); // Small delay to ensure all SVGs are rendered await new Promise(resolve => setTimeout(resolve, 2000)); console.log('Generating PDF...'); await page.pdf({ path: OUTPUT_FILE, format: 'A4', printBackground: true, margin: { top: '20mm', bottom: '20mm', left: '18mm', right: '18mm', }, displayHeaderFooter: true, headerTemplate: `
maintainable.xyz — House Specification Baufritz Build
`, footerTemplate: `
/
`, }); await browser.close(); console.log(`PDF generated: ${OUTPUT_FILE}`); } main().catch(err => { console.error('Build failed:', err); process.exit(1); });