feat: ajouté support markdown dans export PDF
- Parser markdown avec marked pour rendu HTML complet - Styles CSS pour éléments markdown (headings, listes, code, etc.) - Nettoyage automatique des styles temporaires - Fallback texte brut si parsing markdown échoue
This commit is contained in:
@@ -7,6 +7,45 @@ import CircularProgress from '@mui/material/CircularProgress'
|
|||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'
|
||||||
import {formatDate} from '@/lib/format.js'
|
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', '<br>')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamic import of markdown parser
|
||||||
|
const {marked} = await import('marked')
|
||||||
|
// Configure marked for better PDF rendering
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true, // Convert \n to <br>
|
||||||
|
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', '<br>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ExportPdfButton({versionData, size = 'medium', variant = 'outlined'}) {
|
export default function ExportPdfButton({versionData, size = 'medium', variant = 'outlined'}) {
|
||||||
const [isExporting, setIsExporting] = useState(false)
|
const [isExporting, setIsExporting] = useState(false)
|
||||||
|
|
||||||
@@ -32,12 +71,83 @@ export default function ExportPdfButton({versionData, size = 'medium', variant =
|
|||||||
z-index: -1;
|
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 authorName = versionData.user_created?.split('-')[0] || 'Système'
|
||||||
const createdAt = new Date(versionData.date_created)
|
const createdAt = new Date(versionData.date_created)
|
||||||
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
|
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
|
||||||
const voteStatus = createdAt < threeDaysAgo ? 'fermé' : 'ouvert'
|
const voteStatus = createdAt < threeDaysAgo ? 'fermé' : 'ouvert'
|
||||||
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
|
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
|
||||||
|
|
||||||
|
// Render markdown content to HTML
|
||||||
|
const renderedContent = await renderMarkdownToHtml(versionData.delta?.contenu)
|
||||||
|
|
||||||
tempDiv.innerHTML = `
|
tempDiv.innerHTML = `
|
||||||
<div style="margin-bottom: 30px; border-bottom: 2px solid #1976d2; padding-bottom: 20px;">
|
<div style="margin-bottom: 30px; border-bottom: 2px solid #1976d2; padding-bottom: 20px;">
|
||||||
<h1 style="color: #1976d2; margin: 0 0 10px 0; font-size: 28px; font-weight: bold;">
|
<h1 style="color: #1976d2; margin: 0 0 10px 0; font-size: 28px; font-weight: bold;">
|
||||||
@@ -61,8 +171,8 @@ export default function ExportPdfButton({versionData, size = 'medium', variant =
|
|||||||
<h2 style="color: #333; font-size: 20px; margin-bottom: 15px; font-weight: bold;">
|
<h2 style="color: #333; font-size: 20px; margin-bottom: 15px; font-weight: bold;">
|
||||||
Contenu
|
Contenu
|
||||||
</h2>
|
</h2>
|
||||||
<div style="white-space: pre-wrap; font-size: 14px; line-height: 1.8; text-align: justify;">
|
<div class="pdf-content" style="font-size: 14px; line-height: 1.8; text-align: justify;">
|
||||||
${versionData.delta?.contenu || 'Aucun contenu disponible'}
|
${renderedContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -84,6 +194,7 @@ export default function ExportPdfButton({versionData, size = 'medium', variant =
|
|||||||
})
|
})
|
||||||
|
|
||||||
tempDiv.remove()
|
tempDiv.remove()
|
||||||
|
styleElement.remove() // Clean up styles
|
||||||
|
|
||||||
// Créer le PDF
|
// Créer le PDF
|
||||||
const imgData = canvas.toDataURL('image/png', 1)
|
const imgData = canvas.toDataURL('image/png', 1)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
|
"marked": "^16.1.1",
|
||||||
"next": "^14.2.3",
|
"next": "^14.2.3",
|
||||||
"next-auth": "^5.0.0-beta.18",
|
"next-auth": "^5.0.0-beta.18",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -3195,6 +3195,11 @@ markdown-table@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a"
|
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a"
|
||||||
integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==
|
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:
|
mdast-util-find-and-replace@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df"
|
resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df"
|
||||||
|
|||||||
Reference in New Issue
Block a user