Augustin 4a8b6d1cf8 Amélioration complète de l'application web Journal de Conception
- 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>
2025-09-25 12:09:49 +02:00

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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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;