#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { marked } = require('marked'); const puppeteer = require('puppeteer'); const inputFile = process.argv[2]; if (!inputFile) { console.error('Usage: node md-to-pdf.js [output/custom.pdf]'); process.exit(1); } if (!fs.existsSync(inputFile)) { console.error(`File not found: ${inputFile}`); process.exit(1); } const outputDir = path.resolve(path.dirname(inputFile), 'output'); if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); const baseName = path.basename(inputFile, path.extname(inputFile)) + '.pdf'; const outputFile = process.argv[3] ? path.resolve(process.argv[3]) : path.join(outputDir, baseName); const rawMarkdown = fs.readFileSync(inputFile, 'utf8'); // Remove a thematic break (---) when it is immediately followed by a heading // (with only blank lines in between), so it doesn't render as
. const markdown = rawMarkdown.replace(/^---[ \t]*$(\n[ \t]*)*(?=^#{1,6}\s)/gm, ''); const body = marked(markdown); const html = `
${body}
`; (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); const page = await browser.newPage(); // Viewport très haut pour que getBoundingClientRect() retourne des coords absolues await page.setViewport({ width: 794, height: 100000 }); await page.setContent(html, { waitUntil: 'networkidle0' }); // Si un h2 tombe dans la moitié basse d'une page A4, on le pousse à la page suivante. // On simule la pagination avec un décalage cumulatif : chaque saut de page forcé // avance les positions de tous les titres suivants, évitant l'effet cascade. await page.evaluate(() => { const MM_TO_PX = 96 / 25.4; const PAGE_H = Math.round(297 * MM_TO_PX); // 1123px — hauteur brute A4 const MARGIN_TOP = Math.round(15 * MM_TO_PX); // 57px const MARGIN_BOT = Math.round(15 * MM_TO_PX); // 57px const CONTENT_H = PAGE_H - MARGIN_TOP - MARGIN_BOT; // ~1009px par page let cumulativeShift = 0; document.querySelectorAll('h2').forEach(h => { // Position originale (layout écran, sans sauts de page CSS) const originalY = h.getBoundingClientRect().top; // Position effective après les sauts de page déjà décidés avant ce titre const effectiveY = originalY + cumulativeShift; const pageNum = Math.floor(effectiveY / CONTENT_H); const posInPage = effectiveY % CONTENT_H; if (posInPage > CONTENT_H / 2) { h.style.pageBreakBefore = 'always'; // Ce titre est repoussé au début de la page suivante : // on calcule le décalage ajouté et on l'accumule pour les titres suivants const newEffectiveY = (pageNum + 1) * CONTENT_H; cumulativeShift += newEffectiveY - effectiveY; } }); }); await page.pdf({ path: outputFile, format: 'A4', printBackground: true, margin: { top: '15mm', bottom: '15mm', left: '10mm', right: '10mm' }, }); await browser.close(); console.log(`PDF generated: ${outputFile}`); })();