rajoute le support de l'utilisation de markdown (sur un sous-ensemble) dans la description des campagnes et des propositions
This commit is contained in:
182
src/lib/markdown.ts
Normal file
182
src/lib/markdown.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Fonction pour valider les URLs
|
||||
function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
// Autoriser seulement http et https
|
||||
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour nettoyer et valider le contenu markdown
|
||||
export function parseMarkdown(content: string): string {
|
||||
if (!content) return '';
|
||||
|
||||
// Nettoyer le contenu avant parsing
|
||||
const cleanContent = content.trim();
|
||||
|
||||
// Parser le markdown avec des regex simples et sécurisées
|
||||
let htmlContent = cleanContent
|
||||
// Échapper les caractères HTML
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
// Parser le markdown de base
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/__(.*?)__/g, '<u>$1</u>')
|
||||
.replace(/~~(.*?)~~/g, '<del>$1</del>')
|
||||
// Parser les liens avec validation
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||||
if (isValidUrl(url)) {
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
}
|
||||
return text; // Retourner juste le texte si l'URL n'est pas valide
|
||||
})
|
||||
// Parser les titres
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
// Parser les listes
|
||||
.replace(/^- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/^\d+\. (.*$)/gim, '<li>$1</li>')
|
||||
// Parser les paragraphes
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
// Ajouter les balises de paragraphe si nécessaire
|
||||
if (!htmlContent.startsWith('<h') && !htmlContent.startsWith('<li')) {
|
||||
htmlContent = `<p>${htmlContent}</p>`;
|
||||
}
|
||||
|
||||
// Configurer DOMPurify pour autoriser seulement les éléments sécurisés
|
||||
const cleanHtml = DOMPurify.sanitize(htmlContent, {
|
||||
ALLOWED_TAGS: [
|
||||
// Texte de base
|
||||
'p', 'br', 'strong', 'em', 'u', 'del',
|
||||
// Listes
|
||||
'ul', 'ol', 'li',
|
||||
// Liens (avec validation)
|
||||
'a',
|
||||
// Titres (limités)
|
||||
'h1', 'h2', 'h3',
|
||||
// Saut de ligne
|
||||
'hr'
|
||||
],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
|
||||
});
|
||||
|
||||
// Valider et nettoyer les liens
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = cleanHtml;
|
||||
|
||||
// Valider tous les liens
|
||||
const links = tempDiv.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && !isValidUrl(href)) {
|
||||
// Supprimer le lien si l'URL n'est pas valide
|
||||
link.removeAttribute('href');
|
||||
link.style.pointerEvents = 'none';
|
||||
link.style.color = '#999';
|
||||
} else if (href) {
|
||||
// Ajouter target="_blank" et rel="noopener noreferrer" pour la sécurité
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
});
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
// Fonction pour prévisualiser le markdown (version simplifiée)
|
||||
export function previewMarkdown(content: string): string {
|
||||
if (!content) return '';
|
||||
|
||||
// Remplacer les caractères spéciaux pour la prévisualisation
|
||||
return content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/__(.*?)__/g, '<u>$1</u>')
|
||||
.replace(/~~(.*?)~~/g, '<del>$1</del>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/^\d+\. (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// Fonction pour valider le contenu markdown avant sauvegarde
|
||||
export function validateMarkdown(content: string): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!content) {
|
||||
return { isValid: true, errors: [] };
|
||||
}
|
||||
|
||||
// Vérifier la longueur
|
||||
if (content.length > 5000) {
|
||||
errors.push('Le contenu est trop long (maximum 5000 caractères)');
|
||||
}
|
||||
|
||||
// Vérifier les liens malveillants
|
||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
let match;
|
||||
while ((match = linkRegex.exec(content)) !== null) {
|
||||
const url = match[2];
|
||||
if (!isValidUrl(url)) {
|
||||
errors.push(`URL invalide détectée: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les balises HTML non autorisées
|
||||
const forbiddenTags = ['<script', '<style', '<iframe', '<object', '<embed', '<form'];
|
||||
forbiddenTags.forEach(tag => {
|
||||
if (content.toLowerCase().includes(tag)) {
|
||||
errors.push(`Balise non autorisée détectée: ${tag}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
// Fonction pour obtenir un aperçu du contenu (sans HTML)
|
||||
export function getMarkdownPreview(content: string, maxLength: number = 150): string {
|
||||
if (!content) return '';
|
||||
|
||||
// Supprimer le markdown pour l'aperçu
|
||||
const plainText = content
|
||||
.replace(/\*\*(.*?)\*\*/g, '$1')
|
||||
.replace(/\*(.*?)\*/g, '$1')
|
||||
.replace(/__(.*?)__/g, '$1')
|
||||
.replace(/~~(.*?)~~/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/^[#\-\d\.\s]+/gm, '')
|
||||
.replace(/\n+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (plainText.length <= maxLength) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return plainText.substring(0, maxLength) + '...';
|
||||
}
|
||||
Reference in New Issue
Block a user