diff --git a/components/versions/export-pdf-button.js b/components/versions/export-pdf-button.js index f6b12f5..d2fc2e7 100644 --- a/components/versions/export-pdf-button.js +++ b/components/versions/export-pdf-button.js @@ -7,6 +7,45 @@ import CircularProgress from '@mui/material/CircularProgress' import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf' import {formatDate} from '@/lib/format.js' +// Helper function to render markdown to HTML +const renderMarkdownToHtml = async content => { + if (!content) { + return 'Aucun contenu disponible' + } + + // Check if content contains markdown syntax + const hasMarkdown = content.includes('**') + || content.includes('*') + || content.includes('#') + || content.includes('[') + || content.includes('`') + || content.includes('> ') + || content.includes('- ') + || content.includes('1. ') + + if (!hasMarkdown) { + // Simple text with line breaks + return content.replaceAll('\n', '
') + } + + try { + // Dynamic import of markdown parser + const {marked} = await import('marked') + // Configure marked for better PDF rendering + marked.setOptions({ + breaks: true, // Convert \n to
+ gfm: true, // GitHub flavored markdown + headerIds: false, // No header IDs needed for PDF + mangle: false // Don't mangle email addresses + }) + + return marked(content) + } catch (error) { + console.warn('Failed to parse markdown, falling back to plain text:', error) + return content.replaceAll('\n', '
') + } +} + export default function ExportPdfButton({versionData, size = 'medium', variant = 'outlined'}) { const [isExporting, setIsExporting] = useState(false) @@ -32,12 +71,83 @@ export default function ExportPdfButton({versionData, size = 'medium', variant = z-index: -1; ` + // Add CSS styles for markdown elements + const styleElement = document.createElement('style') + styleElement.textContent = ` + .pdf-content h1, .pdf-content h2, .pdf-content h3, .pdf-content h4, .pdf-content h5, .pdf-content h6 { + margin: 20px 0 10px 0; + font-weight: bold; + color: #333; + } + .pdf-content h1 { font-size: 24px; color: #1976d2; } + .pdf-content h2 { font-size: 20px; } + .pdf-content h3 { font-size: 18px; } + .pdf-content h4 { font-size: 16px; } + .pdf-content p { margin: 10px 0; } + .pdf-content strong, .pdf-content b { font-weight: bold; } + .pdf-content em, .pdf-content i { font-style: italic; } + .pdf-content ul, .pdf-content ol { + margin: 10px 0; + padding-left: 25px; + } + .pdf-content li { margin: 5px 0; } + .pdf-content blockquote { + margin: 15px 0; + padding: 10px 20px; + border-left: 4px solid #1976d2; + background-color: #f5f5f5; + font-style: italic; + } + .pdf-content code { + background-color: #f5f5f5; + padding: 2px 4px; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 13px; + } + .pdf-content pre { + background-color: #f5f5f5; + padding: 15px; + border-radius: 5px; + overflow-x: auto; + margin: 15px 0; + } + .pdf-content pre code { + background: none; + padding: 0; + } + .pdf-content a { color: #1976d2; text-decoration: underline; } + .pdf-content hr { + border: none; + border-top: 1px solid #ccc; + margin: 20px 0; + } + .pdf-content table { + border-collapse: collapse; + margin: 15px 0; + width: 100%; + } + .pdf-content th, .pdf-content td { + border: 1px solid #ccc; + padding: 8px; + text-align: left; + } + .pdf-content th { + background-color: #f5f5f5; + font-weight: bold; + } + ` + document.head.append(styleElement) + const authorName = versionData.user_created?.split('-')[0] || 'Système' const createdAt = new Date(versionData.date_created) const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000)) const voteStatus = createdAt < threeDaysAgo ? 'fermé' : 'ouvert' const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f' + // Render markdown content to HTML + const renderedContent = await renderMarkdownToHtml(versionData.delta?.contenu) + tempDiv.innerHTML = `

@@ -61,8 +171,8 @@ export default function ExportPdfButton({versionData, size = 'medium', variant =

Contenu

-
- ${versionData.delta?.contenu || 'Aucun contenu disponible'} +
+ ${renderedContent}
@@ -84,6 +194,7 @@ export default function ExportPdfButton({versionData, size = 'medium', variant = }) tempDiv.remove() + styleElement.remove() // Clean up styles // Créer le PDF const imgData = canvas.toDataURL('image/png', 1) diff --git a/package.json b/package.json index 6d65a15..6f59771 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "date-fns": "^3.6.0", "html2canvas": "^1.4.1", "jspdf": "^3.0.1", + "marked": "^16.1.1", "next": "^14.2.3", "next-auth": "^5.0.0-beta.18", "react": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index 22e4471..ac5011e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3195,6 +3195,11 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== +marked@^16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-16.1.1.tgz#a7839dcf19fa5e349cad12c561f231320690acd4" + integrity sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ== + mdast-util-find-and-replace@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df"