
- Refonte complète du design avec système de panneaux latéraux rétractables - Ajout de templates de projets par domaine (recherche, informatique, mathématiques, etc.) - Implémentation système d'export PDF avec Puppeteer - Amélioration de l'API REST avec nouvelles routes d'export et templates - Ajout de JavaScript client pour interactions dynamiques - Configuration environnement étendue pour futures fonctionnalités IA - Amélioration responsive design et expérience utilisateur 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
512 lines
12 KiB
JavaScript
512 lines
12 KiB
JavaScript
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: `
|
|
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-top: 1cm;">
|
|
<span>${title}</span>
|
|
</div>
|
|
`,
|
|
footerTemplate: `
|
|
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-bottom: 1cm;">
|
|
<span>Page <span class="pageNumber"></span> sur <span class="totalPages"></span> - Généré le ${new Date().toLocaleDateString('fr-FR')}</span>
|
|
</div>
|
|
`
|
|
});
|
|
|
|
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, '<div class="mermaid">$1</div>');
|
|
|
|
// 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 `<pre><code${language}>${escapeHtml(code)}</code></pre>`;
|
|
});
|
|
|
|
// Code en ligne
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
|
|
// Traitement des tableaux amélioré
|
|
html = html.replace(/(\|.*\|\s*\n)+/g, (match) => {
|
|
const lines = match.trim().split('\n').filter(line => line.trim());
|
|
let tableHtml = '<table>\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 += ' <tr>\n';
|
|
cells.forEach(cell => {
|
|
tableHtml += ` <${tag}>${cell}</${tag}>\n`;
|
|
});
|
|
tableHtml += ' </tr>\n';
|
|
|
|
if (inHeader && index === 0) inHeader = false;
|
|
});
|
|
|
|
tableHtml += '</table>\n';
|
|
return tableHtml;
|
|
});
|
|
|
|
// Conversion markdown vers HTML
|
|
html = html
|
|
// Titres (en ordre décroissant)
|
|
.replace(/^#{6}\s+(.*$)/gm, '<h6>$1</h6>')
|
|
.replace(/^#{5}\s+(.*$)/gm, '<h5>$1</h5>')
|
|
.replace(/^#{4}\s+(.*$)/gm, '<h4>$1</h4>')
|
|
.replace(/^#{3}\s+(.*$)/gm, '<h3>$1</h3>')
|
|
.replace(/^#{2}\s+(.*$)/gm, '<h2>$1</h2>')
|
|
.replace(/^#{1}\s+(.*$)/gm, '<h1>$1</h1>')
|
|
// Citations
|
|
.replace(/^>\s+(.*$)/gm, '<blockquote>$1</blockquote>')
|
|
// Gras et italique (ordre important)
|
|
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
// Barré
|
|
.replace(/~~(.*?)~~/g, '<del>$1</del>')
|
|
// Listes numérotées
|
|
.replace(/^(\s*)(\d+)\. (.*$)/gm, '<li class="numbered">$3</li>')
|
|
// Listes à puces et tâches
|
|
.replace(/^(\s*)- \[x\] (.*$)/gm, '<li class="todo-done">✅ $2</li>')
|
|
.replace(/^(\s*)- \[ \] (.*$)/gm, '<li class="todo">☐ $2</li>')
|
|
.replace(/^(\s*)[*\-+] (.*$)/gm, '<li>$2</li>')
|
|
// Liens
|
|
.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
// Ligne horizontale
|
|
.replace(/^---+$/gm, '<hr>');
|
|
|
|
// Grouper les listes consécutives
|
|
html = html.replace(/(<li[^>]*>.*?<\/li>\s*)+/gs, (match) => {
|
|
if (match.includes('class="numbered"')) {
|
|
return '<ol>' + match.replace(/ class="numbered"/g, '') + '</ol>';
|
|
} else {
|
|
return '<ul>' + match + '</ul>';
|
|
}
|
|
});
|
|
|
|
// Nettoyer les listes multiples consécutives
|
|
html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, '');
|
|
|
|
// Grouper les citations consécutives
|
|
html = html.replace(/(<blockquote>.*?<\/blockquote>\s*)+/gs, (match) => {
|
|
const content = match.replace(/<\/?blockquote>/g, '');
|
|
return '<blockquote>' + content + '</blockquote>';
|
|
});
|
|
|
|
// 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 '<p>' + paragraph.replace(/\n/g, '<br>') + '</p>';
|
|
}).join('\n\n');
|
|
|
|
const styledHTML = `
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
|
<script>
|
|
mermaid.initialize({
|
|
startOnLoad: true,
|
|
theme: 'default',
|
|
themeVariables: {
|
|
primaryColor: '#0969da',
|
|
primaryTextColor: '#24292f',
|
|
primaryBorderColor: '#0969da',
|
|
lineColor: '#656d76',
|
|
sectionBkgColor: '#f6f8fa',
|
|
altSectionBkgColor: '#ffffff',
|
|
gridColor: '#d1d9e0',
|
|
secondaryColor: '#f6f8fa',
|
|
tertiaryColor: '#0969da'
|
|
},
|
|
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
|
|
fontSize: 14
|
|
});
|
|
</script>
|
|
<style>
|
|
@page {
|
|
size: A4;
|
|
margin: 2cm;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #24292f;
|
|
font-size: 11pt;
|
|
background: white;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
/* Titres style GitHub */
|
|
h1 {
|
|
color: #24292f;
|
|
font-size: 2em;
|
|
font-weight: 600;
|
|
margin: 0 0 16pt 0;
|
|
padding-bottom: 8pt;
|
|
border-bottom: 1px solid #d1d9e0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h2 {
|
|
color: #24292f;
|
|
font-size: 1.5em;
|
|
font-weight: 600;
|
|
margin: 24pt 0 12pt 0;
|
|
padding-bottom: 6pt;
|
|
border-bottom: 1px solid #d1d9e0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h3 {
|
|
color: #24292f;
|
|
font-size: 1.25em;
|
|
font-weight: 600;
|
|
margin: 18pt 0 8pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h4 {
|
|
color: #24292f;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
margin: 16pt 0 6pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h5, h6 {
|
|
color: #24292f;
|
|
font-size: 0.875em;
|
|
font-weight: 600;
|
|
margin: 14pt 0 6pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
/* Paragraphes */
|
|
p {
|
|
margin: 0 0 12pt 0;
|
|
orphans: 2;
|
|
widows: 2;
|
|
}
|
|
|
|
/* Listes */
|
|
ul, ol {
|
|
margin: 0 0 12pt 0;
|
|
padding-left: 2em;
|
|
}
|
|
|
|
ul {
|
|
list-style-type: disc;
|
|
}
|
|
|
|
ol {
|
|
list-style-type: decimal;
|
|
}
|
|
|
|
li {
|
|
margin: 0 0 4pt 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Tâches */
|
|
li.todo {
|
|
list-style: none;
|
|
margin-left: -2em;
|
|
padding-left: 1.5em;
|
|
}
|
|
|
|
li.todo-done {
|
|
list-style: none;
|
|
margin-left: -2em;
|
|
padding-left: 1.5em;
|
|
color: #656d76;
|
|
}
|
|
|
|
/* Mise en forme du texte */
|
|
strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
em {
|
|
font-style: italic;
|
|
}
|
|
|
|
del {
|
|
text-decoration: line-through;
|
|
color: #656d76;
|
|
}
|
|
|
|
/* Liens */
|
|
a {
|
|
color: #0969da;
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Citations */
|
|
blockquote {
|
|
margin: 12pt 0;
|
|
padding: 0 12pt;
|
|
border-left: 4px solid #d1d9e0;
|
|
color: #656d76;
|
|
}
|
|
|
|
blockquote > :first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
blockquote > :last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Ligne horizontale */
|
|
hr {
|
|
margin: 24pt 0;
|
|
border: none;
|
|
border-top: 1px solid #d1d9e0;
|
|
height: 0;
|
|
}
|
|
|
|
/* Tableaux style GitHub */
|
|
table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 12pt 0;
|
|
font-size: 10.5pt;
|
|
border: 1px solid #d1d9e0;
|
|
}
|
|
|
|
th, td {
|
|
border: 1px solid #d1d9e0;
|
|
padding: 6pt 10pt;
|
|
text-align: left;
|
|
}
|
|
|
|
th {
|
|
background-color: #f6f8fa;
|
|
font-weight: 600;
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: #f6f8fa;
|
|
}
|
|
|
|
/* Code style GitHub */
|
|
code {
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
background-color: rgba(175, 184, 193, 0.2);
|
|
padding: 1pt 4pt;
|
|
border-radius: 3pt;
|
|
font-size: 85%;
|
|
}
|
|
|
|
pre {
|
|
background-color: #f6f8fa;
|
|
padding: 12pt;
|
|
border-radius: 6pt;
|
|
border: 1px solid #d1d9e0;
|
|
margin: 12pt 0;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
font-size: 85%;
|
|
line-height: 1.45;
|
|
overflow: visible;
|
|
}
|
|
|
|
pre code {
|
|
background: transparent;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
font-size: inherit;
|
|
}
|
|
|
|
/* Diagrammes Mermaid */
|
|
.mermaid {
|
|
text-align: center;
|
|
margin: 16pt 0;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
.mermaid svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
/* Éviter les coupures de page */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
p, li {
|
|
page-break-inside: avoid;
|
|
orphans: 2;
|
|
widows: 2;
|
|
}
|
|
|
|
pre, table {
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Améliorer la lisibilité */
|
|
body {
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
${html}
|
|
</body>
|
|
</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; |