const express = require('express'); const router = express.Router(); const puppeteer = require('puppeteer'); const path = require('path'); // POST /api/export/pdf - Générer un PDF depuis le contenu markdown router.post('/pdf', async (req, res) => { const { content, title = 'Journal de Conception' } = req.body; if (!content || content.trim() === '') { return res.status(400).json({ success: false, error: 'Contenu requis pour générer le PDF' }); } let browser; try { // Convertir le markdown en HTML avec styles const htmlContent = generateStyledHTML(content, title); // Lancer Puppeteer browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // Définir le contenu HTML await page.setContent(htmlContent, { waitUntil: 'domcontentloaded', timeout: parseInt(process.env.PDF_TIMEOUT) || 30000 }); // Attendre que Mermaid soit chargé et les diagrammes rendus await new Promise(resolve => setTimeout(resolve, 2000)); // Vérifier si des diagrammes Mermaid existent et attendre qu'ils se chargent try { await page.waitForFunction(() => { const mermaidElements = document.querySelectorAll('.mermaid'); if (mermaidElements.length === 0) return true; return Array.from(mermaidElements).every(el => el.querySelector('svg') || el.textContent.trim() === '' || el.classList.contains('error') ); }, { timeout: 10000 }); } catch (e) { console.log('Timeout waiting for Mermaid, proceeding...'); } // Générer le PDF const pdfBuffer = await page.pdf({ format: 'A4', margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' }, printBackground: true, displayHeaderFooter: true, headerTemplate: `
${title}
`, footerTemplate: `
Page sur - Généré le ${new Date().toLocaleDateString('fr-FR')}
` }); await browser.close(); // Vérifier la taille du PDF const maxSize = parseInt(process.env.PDF_MAX_SIZE) || 10485760; // 10MB par défaut if (pdfBuffer.length > maxSize) { return res.status(413).json({ success: false, error: 'Le PDF généré dépasse la taille maximale autorisée' }); } // Envoyer le PDF res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}.pdf"`); res.setHeader('Content-Length', pdfBuffer.length); res.send(pdfBuffer); } catch (error) { console.error('Erreur génération PDF:', error); if (browser) { await browser.close(); } res.status(500).json({ success: false, error: 'Erreur lors de la génération du PDF' }); } }); // Fonction utilitaire pour échapper le HTML function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } // Fonction pour convertir le markdown en HTML stylé avec support Mermaid function generateStyledHTML(content, title) { let html = content; // Traitement des diagrammes Mermaid AVANT les blocs de code html = html.replace(/```mermaid\n([\s\S]*?)```/g, '
$1
'); // Traitement des blocs de code avec langage html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { const language = lang ? ` class="language-${lang}"` : ''; return `
${escapeHtml(code)}
`; }); // Code en ligne html = html.replace(/`([^`]+)`/g, '$1'); // Traitement des tableaux amélioré html = html.replace(/(\|.*\|\s*\n)+/g, (match) => { const lines = match.trim().split('\n').filter(line => line.trim()); let tableHtml = '\n'; let inHeader = true; lines.forEach((line, index) => { // Ignorer les lignes de séparation if (line.match(/^\s*\|[\s\-:]*\|\s*$/)) { inHeader = false; return; } const cells = line.split('|') .map(cell => cell.trim()) .filter((cell, idx, arr) => idx > 0 && idx < arr.length - 1); if (cells.length === 0) return; const tag = inHeader ? 'th' : 'td'; tableHtml += ' \n'; cells.forEach(cell => { tableHtml += ` <${tag}>${cell}\n`; }); tableHtml += ' \n'; if (inHeader && index === 0) inHeader = false; }); tableHtml += '
\n'; return tableHtml; }); // Conversion markdown vers HTML html = html // Titres (en ordre décroissant) .replace(/^#{6}\s+(.*$)/gm, '
$1
') .replace(/^#{5}\s+(.*$)/gm, '
$1
') .replace(/^#{4}\s+(.*$)/gm, '

$1

') .replace(/^#{3}\s+(.*$)/gm, '

$1

') .replace(/^#{2}\s+(.*$)/gm, '

$1

') .replace(/^#{1}\s+(.*$)/gm, '

$1

') // Citations .replace(/^>\s+(.*$)/gm, '
$1
') // Gras et italique (ordre important) .replace(/\*\*\*(.*?)\*\*\*/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') // Barré .replace(/~~(.*?)~~/g, '$1') // Listes numérotées .replace(/^(\s*)(\d+)\. (.*$)/gm, '
  • $3
  • ') // Listes à puces et tâches .replace(/^(\s*)- \[x\] (.*$)/gm, '
  • ✅ $2
  • ') .replace(/^(\s*)- \[ \] (.*$)/gm, '
  • ☐ $2
  • ') .replace(/^(\s*)[*\-+] (.*$)/gm, '
  • $2
  • ') // Liens .replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1') // Ligne horizontale .replace(/^---+$/gm, '
    '); // Grouper les listes consécutives html = html.replace(/(]*>.*?<\/li>\s*)+/gs, (match) => { if (match.includes('class="numbered"')) { return '
      ' + match.replace(/ class="numbered"/g, '') + '
    '; } else { return ''; } }); // Nettoyer les listes multiples consécutives html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, ''); // Grouper les citations consécutives html = html.replace(/(
    .*?<\/blockquote>\s*)+/gs, (match) => { const content = match.replace(/<\/?blockquote>/g, ''); return '
    ' + content + '
    '; }); // Traiter les paragraphes html = html.split('\n\n').map(paragraph => { paragraph = paragraph.trim(); if (!paragraph) return ''; // Ne pas entourer les éléments de bloc dans des paragraphes if (paragraph.match(/^<(h[1-6]|div|table|ul|ol|blockquote|hr|pre)/)) { return paragraph; } return '

    ' + paragraph.replace(/\n/g, '
    ') + '

    '; }).join('\n\n'); const styledHTML = ` ${title} ${html} `; return styledHTML; } // Fonction pour nettoyer le nom de fichier function sanitizeFilename(filename) { return filename .replace(/[^\w\s-]/g, '') // Supprimer caractères spéciaux .replace(/\s+/g, '-') // Remplacer espaces par tirets .toLowerCase() // Minuscules .substring(0, 100); // Limiter la longueur } module.exports = router;