|  | #!/usr/bin/env node | 
					
						
						|  | import { spawn } from 'node:child_process'; | 
					
						
						|  | import { setTimeout as delay } from 'node:timers/promises'; | 
					
						
						|  | import { chromium } from 'playwright'; | 
					
						
						|  | import { resolve } from 'node:path'; | 
					
						
						|  | import { promises as fs } from 'node:fs'; | 
					
						
						|  | import process from 'node:process'; | 
					
						
						|  |  | 
					
						
						|  | async function run(command, args = [], options = {}) { | 
					
						
						|  | return new Promise((resolvePromise, reject) => { | 
					
						
						|  | const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options }); | 
					
						
						|  | child.on('error', reject); | 
					
						
						|  | child.on('exit', (code) => { | 
					
						
						|  | if (code === 0) resolvePromise(undefined); | 
					
						
						|  | else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`)); | 
					
						
						|  | }); | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForServer(url, timeoutMs = 60000) { | 
					
						
						|  | const start = Date.now(); | 
					
						
						|  | while (Date.now() - start < timeoutMs) { | 
					
						
						|  | try { | 
					
						
						|  | const res = await fetch(url); | 
					
						
						|  | if (res.ok) return; | 
					
						
						|  | } catch {} | 
					
						
						|  | await delay(500); | 
					
						
						|  | } | 
					
						
						|  | throw new Error(`Server did not start in time: ${url}`); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | function parseArgs(argv) { | 
					
						
						|  | const out = {}; | 
					
						
						|  | for (const arg of argv.slice(2)) { | 
					
						
						|  | if (!arg.startsWith('--')) continue; | 
					
						
						|  | const [k, v] = arg.replace(/^--/, '').split('='); | 
					
						
						|  | out[k] = v === undefined ? true : v; | 
					
						
						|  | } | 
					
						
						|  | return out; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | function slugify(text) { | 
					
						
						|  | return String(text || '') | 
					
						
						|  | .normalize('NFKD') | 
					
						
						|  | .replace(/\p{Diacritic}+/gu, '') | 
					
						
						|  | .toLowerCase() | 
					
						
						|  | .replace(/[^a-z0-9]+/g, '-') | 
					
						
						|  | .replace(/^-+|-+$/g, '') | 
					
						
						|  | .slice(0, 120) || 'article'; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | function parseMargin(margin) { | 
					
						
						|  | if (!margin) return { top: '12mm', right: '12mm', bottom: '16mm', left: '12mm' }; | 
					
						
						|  | const parts = String(margin).split(',').map(s => s.trim()).filter(Boolean); | 
					
						
						|  | if (parts.length === 1) { | 
					
						
						|  | return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] }; | 
					
						
						|  | } | 
					
						
						|  | if (parts.length === 2) { | 
					
						
						|  | return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] }; | 
					
						
						|  | } | 
					
						
						|  | if (parts.length === 3) { | 
					
						
						|  | return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[1] }; | 
					
						
						|  | } | 
					
						
						|  | return { top: parts[0] || '12mm', right: parts[1] || '12mm', bottom: parts[2] || '16mm', left: parts[3] || '12mm' }; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | function cssLengthToMm(val) { | 
					
						
						|  | if (!val) return 0; | 
					
						
						|  | const s = String(val).trim(); | 
					
						
						|  | if (/mm$/i.test(s)) return parseFloat(s); | 
					
						
						|  | if (/cm$/i.test(s)) return parseFloat(s) * 10; | 
					
						
						|  | if (/in$/i.test(s)) return parseFloat(s) * 25.4; | 
					
						
						|  | if (/px$/i.test(s)) return (parseFloat(s) / 96) * 25.4; | 
					
						
						|  | const num = parseFloat(s); | 
					
						
						|  | return Number.isFinite(num) ? num : 0; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | function getFormatSizeMm(format) { | 
					
						
						|  | const f = String(format || 'A4').toLowerCase(); | 
					
						
						|  | switch (f) { | 
					
						
						|  | case 'letter': return { w: 215.9, h: 279.4 }; | 
					
						
						|  | case 'legal': return { w: 215.9, h: 355.6 }; | 
					
						
						|  | case 'a3': return { w: 297, h: 420 }; | 
					
						
						|  | case 'tabloid': return { w: 279.4, h: 431.8 }; | 
					
						
						|  | case 'a4': | 
					
						
						|  | default: return { w: 210, h: 297 }; | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForImages(page, timeoutMs = 15000) { | 
					
						
						|  | await page.evaluate(async (timeout) => { | 
					
						
						|  | const deadline = Date.now() + timeout; | 
					
						
						|  | const imgs = Array.from(document.images || []); | 
					
						
						|  | const unloaded = imgs.filter(img => !img.complete || (img.naturalWidth === 0)); | 
					
						
						|  | await Promise.race([ | 
					
						
						|  | Promise.all(unloaded.map(img => new Promise(res => { | 
					
						
						|  | if (img.complete && img.naturalWidth !== 0) return res(undefined); | 
					
						
						|  | img.addEventListener('load', () => res(undefined), { once: true }); | 
					
						
						|  | img.addEventListener('error', () => res(undefined), { once: true }); | 
					
						
						|  | }))), | 
					
						
						|  | new Promise(res => setTimeout(res, Math.max(0, deadline - Date.now()))) | 
					
						
						|  | ]); | 
					
						
						|  | }, timeoutMs); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForPlotly(page, timeoutMs = 20000) { | 
					
						
						|  | await page.evaluate(async (timeout) => { | 
					
						
						|  | const start = Date.now(); | 
					
						
						|  | const hasPlots = () => Array.from(document.querySelectorAll('.js-plotly-plot')).length > 0; | 
					
						
						|  |  | 
					
						
						|  | while (!hasPlots() && (Date.now() - start) < timeout) { | 
					
						
						|  | await new Promise(r => setTimeout(r, 200)); | 
					
						
						|  | } | 
					
						
						|  | const deadline = start + timeout; | 
					
						
						|  |  | 
					
						
						|  | const allReady = () => Array.from(document.querySelectorAll('.js-plotly-plot')).every(el => el.querySelector('svg.main-svg')); | 
					
						
						|  | while (!allReady() && Date.now() < deadline) { | 
					
						
						|  | await new Promise(r => setTimeout(r, 200)); | 
					
						
						|  | } | 
					
						
						|  | }, timeoutMs); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForD3(page, timeoutMs = 20000) { | 
					
						
						|  | await page.evaluate(async (timeout) => { | 
					
						
						|  | const start = Date.now(); | 
					
						
						|  | const isReady = () => { | 
					
						
						|  |  | 
					
						
						|  | const hero = document.querySelector('.hero-banner'); | 
					
						
						|  | if (hero) { | 
					
						
						|  | return !!hero.querySelector('svg circle, svg path, svg rect, svg g'); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | const containers = [ | 
					
						
						|  | ...Array.from(document.querySelectorAll('.d3-line')), | 
					
						
						|  | ...Array.from(document.querySelectorAll('.d3-bar')) | 
					
						
						|  | ]; | 
					
						
						|  | if (!containers.length) return true; | 
					
						
						|  | return containers.every(c => c.querySelector('svg circle, svg path, svg rect, svg g')); | 
					
						
						|  | }; | 
					
						
						|  | while (!isReady() && (Date.now() - start) < timeout) { | 
					
						
						|  | await new Promise(r => setTimeout(r, 200)); | 
					
						
						|  | } | 
					
						
						|  | }, timeoutMs); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForStableLayout(page, timeoutMs = 5000) { | 
					
						
						|  | const start = Date.now(); | 
					
						
						|  | let last = await page.evaluate(() => document.scrollingElement ? document.scrollingElement.scrollHeight : document.body.scrollHeight); | 
					
						
						|  | let stableCount = 0; | 
					
						
						|  | while ((Date.now() - start) < timeoutMs && stableCount < 3) { | 
					
						
						|  | await page.waitForTimeout(250); | 
					
						
						|  | const now = await page.evaluate(() => document.scrollingElement ? document.scrollingElement.scrollHeight : document.body.scrollHeight); | 
					
						
						|  | if (now === last) stableCount += 1; else { stableCount = 0; last = now; } | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function waitForTOC(page, timeoutMs = 10000) { | 
					
						
						|  | await page.evaluate(async (timeout) => { | 
					
						
						|  | const start = Date.now(); | 
					
						
						|  | while (Date.now() - start < timeout) { | 
					
						
						|  | const placeholder = document.getElementById('article-toc-placeholder'); | 
					
						
						|  | if (placeholder && placeholder.querySelector('nav ul li')) { | 
					
						
						|  | return; | 
					
						
						|  | } | 
					
						
						|  | await new Promise(r => setTimeout(r, 100)); | 
					
						
						|  | } | 
					
						
						|  | }, timeoutMs); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function convertIframesToImages(page) { | 
					
						
						|  | await page.evaluate(async () => { | 
					
						
						|  | const iframes = Array.from(document.querySelectorAll('iframe')); | 
					
						
						|  | for (const iframe of iframes) { | 
					
						
						|  | try { | 
					
						
						|  | const rect = iframe.getBoundingClientRect(); | 
					
						
						|  | if (rect.width > 0 && rect.height > 0) { | 
					
						
						|  | const placeholder = document.createElement('div'); | 
					
						
						|  | placeholder.style.cssText = ` | 
					
						
						|  | width: ${rect.width}px; | 
					
						
						|  | height: ${rect.height}px; | 
					
						
						|  | background: #f5f5f5; | 
					
						
						|  | border: 1px solid #ddd; | 
					
						
						|  | display: flex; | 
					
						
						|  | align-items: center; | 
					
						
						|  | justify-content: center; | 
					
						
						|  | font-family: system-ui, sans-serif; | 
					
						
						|  | color: #666; | 
					
						
						|  | font-size: 14px; | 
					
						
						|  | `; | 
					
						
						|  | placeholder.textContent = '[Interactive content - view online]'; | 
					
						
						|  | iframe.replaceWith(placeholder); | 
					
						
						|  | } | 
					
						
						|  | } catch (e) { | 
					
						
						|  | console.warn('Could not process iframe:', e); | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | async function main() { | 
					
						
						|  | const cwd = process.cwd(); | 
					
						
						|  | const port = Number(process.env.PREVIEW_PORT || 8080); | 
					
						
						|  | const baseUrl = `http://127.0.0.1:${port}/`; | 
					
						
						|  | const args = parseArgs(process.argv); | 
					
						
						|  |  | 
					
						
						|  | const theme = (args.theme === 'dark' || args.theme === 'light') ? args.theme : 'light'; | 
					
						
						|  | const format = args.format || 'A4'; | 
					
						
						|  | const margin = parseMargin(args.margin); | 
					
						
						|  | const wait = (args.wait || 'full'); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article'; | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | const distDir = resolve(cwd, 'dist'); | 
					
						
						|  | let hasDist = false; | 
					
						
						|  | try { | 
					
						
						|  | const st = await fs.stat(distDir); | 
					
						
						|  | hasDist = st && st.isDirectory(); | 
					
						
						|  | } catch {} | 
					
						
						|  | if (!hasDist) { | 
					
						
						|  | console.log('> Building Astro siteβ¦'); | 
					
						
						|  | await run('npm', ['run', 'build']); | 
					
						
						|  | } else { | 
					
						
						|  | console.log('> Skipping build (dist/ exists)β¦'); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | console.log('> Starting Astro previewβ¦'); | 
					
						
						|  |  | 
					
						
						|  | const preview = spawn('npm', ['run', 'preview'], { cwd, stdio: 'inherit', detached: true }); | 
					
						
						|  | const previewExit = new Promise((resolvePreview) => { | 
					
						
						|  | preview.on('close', (code, signal) => resolvePreview({ code, signal })); | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | await waitForServer(baseUrl, 60000); | 
					
						
						|  | console.log('> Server ready, generating PDFβ¦'); | 
					
						
						|  |  | 
					
						
						|  | const browser = await chromium.launch({ headless: true }); | 
					
						
						|  | try { | 
					
						
						|  | const context = await browser.newContext(); | 
					
						
						|  | await context.addInitScript((desired) => { | 
					
						
						|  | try { | 
					
						
						|  | localStorage.setItem('theme', desired); | 
					
						
						|  |  | 
					
						
						|  | if (document && document.documentElement) { | 
					
						
						|  | document.documentElement.dataset.theme = desired; | 
					
						
						|  | } | 
					
						
						|  | } catch {} | 
					
						
						|  | }, theme); | 
					
						
						|  | const page = await context.newPage(); | 
					
						
						|  |  | 
					
						
						|  | const fmt = getFormatSizeMm(format); | 
					
						
						|  | const mw = fmt.w - cssLengthToMm(margin.left) - cssLengthToMm(margin.right); | 
					
						
						|  | const printableWidthPx = Math.max(320, Math.round((mw / 25.4) * 96)); | 
					
						
						|  | await page.setViewportSize({ width: printableWidthPx, height: 1200 }); | 
					
						
						|  | await page.goto(baseUrl, { waitUntil: 'load', timeout: 60000 }); | 
					
						
						|  |  | 
					
						
						|  | try { await page.waitForFunction(() => !!window.Plotly, { timeout: 8000 }); } catch {} | 
					
						
						|  | try { await page.waitForFunction(() => !!window.d3, { timeout: 8000 }); } catch {} | 
					
						
						|  |  | 
					
						
						|  | if (!args.filename) { | 
					
						
						|  | const fromBtn = await page.evaluate(() => { | 
					
						
						|  | const btn = document.getElementById('download-pdf-btn'); | 
					
						
						|  | const f = btn ? btn.getAttribute('data-pdf-filename') : null; | 
					
						
						|  | return f || ''; | 
					
						
						|  | }); | 
					
						
						|  | if (fromBtn) { | 
					
						
						|  | outFileBase = String(fromBtn).replace(/\.pdf$/i, ''); | 
					
						
						|  | } else { | 
					
						
						|  |  | 
					
						
						|  | const title = await page.evaluate(() => { | 
					
						
						|  | const h1 = document.querySelector('h1.hero-title'); | 
					
						
						|  | const t = h1 ? h1.textContent : document.title; | 
					
						
						|  | return (t || '').replace(/\s+/g, ' ').trim(); | 
					
						
						|  | }); | 
					
						
						|  | outFileBase = slugify(title); | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | if (wait === 'images' || wait === 'full') { | 
					
						
						|  | await waitForImages(page); | 
					
						
						|  | } | 
					
						
						|  | if (wait === 'd3' || wait === 'full') { | 
					
						
						|  | await waitForD3(page); | 
					
						
						|  | } | 
					
						
						|  | if (wait === 'plotly' || wait === 'full') { | 
					
						
						|  | await waitForPlotly(page); | 
					
						
						|  | } | 
					
						
						|  | if (wait === 'full') { | 
					
						
						|  | await waitForTOC(page); | 
					
						
						|  | await waitForStableLayout(page); | 
					
						
						|  | } | 
					
						
						|  | await page.emulateMedia({ media: 'print' }); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | await page.evaluate(() => { | 
					
						
						|  | function isSmallSvg(svg){ | 
					
						
						|  | try { | 
					
						
						|  | const vb = svg && svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal : null; | 
					
						
						|  | if (vb && vb.width && vb.height && vb.width <= 50 && vb.height <= 50) return true; | 
					
						
						|  | const r = svg.getBoundingClientRect && svg.getBoundingClientRect(); | 
					
						
						|  | if (r && r.width && r.height && r.width <= 50 && r.height <= 50) return true; | 
					
						
						|  | } catch {} | 
					
						
						|  | return false; | 
					
						
						|  | } | 
					
						
						|  | function lockSmallSvgSize(svg){ | 
					
						
						|  | try { | 
					
						
						|  | const r = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null; | 
					
						
						|  | const w = (r && r.width) ? Math.round(r.width) : null; | 
					
						
						|  | const h = (r && r.height) ? Math.round(r.height) : null; | 
					
						
						|  | if (w) svg.style.setProperty('width', w + 'px', 'important'); | 
					
						
						|  | if (h) svg.style.setProperty('height', h + 'px', 'important'); | 
					
						
						|  | svg.style.setProperty('max-width', 'none', 'important'); | 
					
						
						|  | } catch {} | 
					
						
						|  | } | 
					
						
						|  | function fixSvg(svg){ | 
					
						
						|  | if (!svg) return; | 
					
						
						|  |  | 
					
						
						|  | try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {} | 
					
						
						|  | if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; } | 
					
						
						|  | try { svg.removeAttribute('width'); } catch {} | 
					
						
						|  | try { svg.removeAttribute('height'); } catch {} | 
					
						
						|  | svg.style.maxWidth = '100%'; | 
					
						
						|  | svg.style.width = '100%'; | 
					
						
						|  | svg.style.height = 'auto'; | 
					
						
						|  | if (!svg.getAttribute('preserveAspectRatio')) svg.setAttribute('preserveAspectRatio','xMidYMid meet'); | 
					
						
						|  | } | 
					
						
						|  | document.querySelectorAll('svg').forEach(fixSvg); | 
					
						
						|  | document.querySelectorAll('.mermaid, .mermaid svg').forEach((el)=>{ | 
					
						
						|  | if (el.tagName && el.tagName.toLowerCase() === 'svg') fixSvg(el); | 
					
						
						|  | else { el.style.display='block'; el.style.width='100%'; el.style.maxWidth='100%'; } | 
					
						
						|  | }); | 
					
						
						|  | document.querySelectorAll('iframe, embed, object').forEach((el) => { | 
					
						
						|  | el.style.width = '100%'; | 
					
						
						|  | el.style.maxWidth = '100%'; | 
					
						
						|  | try { el.removeAttribute('width'); } catch {} | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | const doc = (el.tagName.toLowerCase()==='object' ? el.contentDocument : el.contentDocument); | 
					
						
						|  | if (doc && doc.head) { | 
					
						
						|  | const s = doc.createElement('style'); | 
					
						
						|  | s.textContent = 'html,body{overflow-x:hidden;} svg,canvas,img,video{max-width:100%!important;height:auto!important;} svg[width]{width:100%!important}'; | 
					
						
						|  | doc.head.appendChild(s); | 
					
						
						|  | doc.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); }); | 
					
						
						|  | } | 
					
						
						|  | } catch (_) {  } | 
					
						
						|  | }); | 
					
						
						|  | }); | 
					
						
						|  | } catch {} | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | const ogW = 1200, ogH = 630; | 
					
						
						|  | await page.setViewportSize({ width: ogW, height: ogH }); | 
					
						
						|  |  | 
					
						
						|  | await page.waitForTimeout(200); | 
					
						
						|  |  | 
					
						
						|  | await page.evaluate(() => { window.scrollTo(0, 0); window.dispatchEvent(new Event('resize')); }); | 
					
						
						|  | try { await waitForD3(page, 8000); } catch {} | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | const cssHandle = await page.addStyleTag({ content: ` | 
					
						
						|  | .hero .points { mix-blend-mode: normal !important; } | 
					
						
						|  | ` }); | 
					
						
						|  | const thumbPath = resolve(cwd, 'dist', 'thumb.auto.jpg'); | 
					
						
						|  | await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false }); | 
					
						
						|  |  | 
					
						
						|  | const thumbPngPath = resolve(cwd, 'dist', 'thumb.auto.png'); | 
					
						
						|  | await page.screenshot({ path: thumbPngPath, type: 'png', fullPage: false }); | 
					
						
						|  | const publicThumb = resolve(cwd, 'public', 'thumb.auto.jpg'); | 
					
						
						|  | const publicThumbPng = resolve(cwd, 'public', 'thumb.auto.png'); | 
					
						
						|  | try { await fs.copyFile(thumbPath, publicThumb); } catch {} | 
					
						
						|  | try { await fs.copyFile(thumbPngPath, publicThumbPng); } catch {} | 
					
						
						|  |  | 
					
						
						|  | try { await cssHandle.evaluate((el) => el.remove()); } catch {} | 
					
						
						|  | console.log(`β
 OG thumbnail generated: ${thumbPath}`); | 
					
						
						|  | } catch (e) { | 
					
						
						|  | console.warn('Unable to generate OG thumbnail:', e?.message || e); | 
					
						
						|  | } | 
					
						
						|  | const outPath = resolve(cwd, 'dist', `${outFileBase}.pdf`); | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | const fmt2 = getFormatSizeMm(format); | 
					
						
						|  | const mw2 = fmt2.w - cssLengthToMm(margin.left) - cssLengthToMm(margin.right); | 
					
						
						|  | const printableWidthPx2 = Math.max(320, Math.round((mw2 / 25.4) * 96)); | 
					
						
						|  | await page.setViewportSize({ width: printableWidthPx2, height: 1400 }); | 
					
						
						|  | await page.evaluate(() => { window.scrollTo(0, 0); window.dispatchEvent(new Event('resize')); }); | 
					
						
						|  | try { await waitForD3(page, 8000); } catch {} | 
					
						
						|  | await waitForStableLayout(page); | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | await page.evaluate(() => { | 
					
						
						|  | function isSmallSvg(svg){ | 
					
						
						|  | try { | 
					
						
						|  | const vb = svg && svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal : null; | 
					
						
						|  | if (vb && vb.width && vb.height && vb.width <= 50 && vb.height <= 50) return true; | 
					
						
						|  | const r = svg.getBoundingClientRect && svg.getBoundingClientRect(); | 
					
						
						|  | if (r && r.width && r.height && r.width <= 50 && r.height <= 50) return true; | 
					
						
						|  | } catch {} | 
					
						
						|  | return false; | 
					
						
						|  | } | 
					
						
						|  | function lockSmallSvgSize(svg){ | 
					
						
						|  | try { | 
					
						
						|  | const r = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null; | 
					
						
						|  | const w = (r && r.width) ? Math.round(r.width) : null; | 
					
						
						|  | const h = (r && r.height) ? Math.round(r.height) : null; | 
					
						
						|  | if (w) svg.style.setProperty('width', w + 'px', 'important'); | 
					
						
						|  | if (h) svg.style.setProperty('height', h + 'px', 'important'); | 
					
						
						|  | svg.style.setProperty('max-width', 'none', 'important'); | 
					
						
						|  | } catch {} | 
					
						
						|  | } | 
					
						
						|  | function fixSvg(svg){ | 
					
						
						|  | if (!svg) return; | 
					
						
						|  |  | 
					
						
						|  | try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {} | 
					
						
						|  | if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; } | 
					
						
						|  | try { svg.removeAttribute('width'); } catch {} | 
					
						
						|  | try { svg.removeAttribute('height'); } catch {} | 
					
						
						|  | svg.style.maxWidth = '100%'; | 
					
						
						|  | svg.style.width = '100%'; | 
					
						
						|  | svg.style.height = 'auto'; | 
					
						
						|  | if (!svg.getAttribute('preserveAspectRatio')) svg.setAttribute('preserveAspectRatio','xMidYMid meet'); | 
					
						
						|  | } | 
					
						
						|  | document.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); }); | 
					
						
						|  | document.querySelectorAll('.mermaid, .mermaid svg').forEach((el)=>{ | 
					
						
						|  | if (el.tagName && el.tagName.toLowerCase() === 'svg') fixSvg(el); | 
					
						
						|  | else { el.style.display='block'; el.style.width='100%'; el.style.maxWidth='100%'; } | 
					
						
						|  | }); | 
					
						
						|  | document.querySelectorAll('iframe, embed, object').forEach((el) => { | 
					
						
						|  | el.style.width = '100%'; | 
					
						
						|  | el.style.maxWidth = '100%'; | 
					
						
						|  | try { el.removeAttribute('width'); } catch {} | 
					
						
						|  | try { | 
					
						
						|  | const doc = (el.tagName.toLowerCase()==='object' ? el.contentDocument : el.contentDocument); | 
					
						
						|  | if (doc && doc.head) { | 
					
						
						|  | const s = doc.createElement('style'); | 
					
						
						|  | s.textContent = 'html,body{overflow-x:hidden;} svg,canvas,img,video{max-width:100%!important;height:auto!important;} svg[width]{width:100%!important}'; | 
					
						
						|  | doc.head.appendChild(s); | 
					
						
						|  | doc.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); }); | 
					
						
						|  | } | 
					
						
						|  | } catch (_) {} | 
					
						
						|  | }); | 
					
						
						|  | }); | 
					
						
						|  | } catch {} | 
					
						
						|  | } catch {} | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | await convertIframesToImages(page); | 
					
						
						|  | await page.waitForTimeout(500); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | await page.evaluate(() => { | 
					
						
						|  | document.querySelectorAll('img').forEach(img => { | 
					
						
						|  | if (img.loading === 'lazy') img.loading = 'eager'; | 
					
						
						|  | }); | 
					
						
						|  | }); | 
					
						
						|  | await waitForImages(page, 10000); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | let pdfCssHandle = null; | 
					
						
						|  | try { | 
					
						
						|  | pdfCssHandle = await page.addStyleTag({ content: ` | 
					
						
						|  | /* General container safety */ | 
					
						
						|  | html, body { overflow-x: hidden !important; } | 
					
						
						|  |  | 
					
						
						|  | /* Make all vector/bitmap media responsive for print */ | 
					
						
						|  | svg, canvas, img, video { max-width: 100% !important; height: auto !important; } | 
					
						
						|  | /* Ensure images are visible in PDF */ | 
					
						
						|  | img { | 
					
						
						|  | -webkit-print-color-adjust: exact !important; | 
					
						
						|  | print-color-adjust: exact !important; | 
					
						
						|  | display: block !important; | 
					
						
						|  | } | 
					
						
						|  | /* Mermaid diagrams */ | 
					
						
						|  | .mermaid, .mermaid svg { display: block; width: 100% !important; max-width: 100% !important; height: auto !important; } | 
					
						
						|  | /* Any explicit width attributes */ | 
					
						
						|  | svg[width] { width: 100% !important; } | 
					
						
						|  | /* Iframes and similar embeds */ | 
					
						
						|  | iframe, embed, object { width: 100% !important; max-width: 100% !important; height: auto; } | 
					
						
						|  |  | 
					
						
						|  | /* HtmlEmbed wrappers (defensive) */ | 
					
						
						|  | .html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; } | 
					
						
						|  | .html-embed__card > div[id^="frag-"] { width: 100% !important; max-width: 100% !important; } | 
					
						
						|  |  | 
					
						
						|  | /* Banner centering & visibility */ | 
					
						
						|  | .hero .points { mix-blend-mode: normal !important; } | 
					
						
						|  | /* Do NOT force a fixed height to avoid clipping in PDF */ | 
					
						
						|  | .hero-banner { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; } | 
					
						
						|  | .hero-banner svg { width: 100% !important; height: auto !important; } | 
					
						
						|  | ` }); | 
					
						
						|  | } catch {} | 
					
						
						|  | await page.pdf({ | 
					
						
						|  | path: outPath, | 
					
						
						|  | format, | 
					
						
						|  | printBackground: true, | 
					
						
						|  | margin | 
					
						
						|  | }); | 
					
						
						|  | try { if (pdfCssHandle) await pdfCssHandle.evaluate((el) => el.remove()); } catch {} | 
					
						
						|  | console.log(`β
 PDF generated: ${outPath}`); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`); | 
					
						
						|  | try { | 
					
						
						|  | await fs.mkdir(resolve(cwd, 'public'), { recursive: true }); | 
					
						
						|  | await fs.copyFile(outPath, publicSlugPath); | 
					
						
						|  | console.log(`β
 PDF copied to: ${publicSlugPath}`); | 
					
						
						|  | } catch (e) { | 
					
						
						|  | console.warn('Unable to copy PDF to public/:', e?.message || e); | 
					
						
						|  | } | 
					
						
						|  | } finally { | 
					
						
						|  | await browser.close(); | 
					
						
						|  | } | 
					
						
						|  | } finally { | 
					
						
						|  |  | 
					
						
						|  | try { | 
					
						
						|  | if (process.platform !== 'win32') { | 
					
						
						|  | try { process.kill(-preview.pid, 'SIGINT'); } catch {} | 
					
						
						|  | } | 
					
						
						|  | try { preview.kill('SIGINT'); } catch {} | 
					
						
						|  | await Promise.race([previewExit, delay(3000)]); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | if (!preview.killed) { | 
					
						
						|  | try { | 
					
						
						|  | if (process.platform !== 'win32') { | 
					
						
						|  | try { process.kill(-preview.pid, 'SIGKILL'); } catch {} | 
					
						
						|  | } | 
					
						
						|  | try { preview.kill('SIGKILL'); } catch {} | 
					
						
						|  | } catch {} | 
					
						
						|  | await Promise.race([previewExit, delay(1000)]); | 
					
						
						|  | } | 
					
						
						|  | } catch {} | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | main().catch((err) => { | 
					
						
						|  | console.error(err); | 
					
						
						|  | process.exit(1); | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  |