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}${tag}>\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;