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(); // Diviser le contenu en lignes pour traiter les listes correctement const lines = cleanContent.split('\n'); const processedLines: string[] = []; let inUnorderedList = false; let inOrderedList = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Échapper les caractères HTML let processedLine = line .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // Parser le markdown de base processedLine = processedLine .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/__(.*?)__/g, '$1') .replace(/~~(.*?)~~/g, '$1') // Parser les liens avec validation .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { if (isValidUrl(url)) { return `${text}`; } return text; // Retourner juste le texte si l'URL n'est pas valide }); // Traiter les titres if (processedLine.match(/^### /)) { processedLine = processedLine.replace(/^### (.*$)/, '

$1

'); } else if (processedLine.match(/^## /)) { processedLine = processedLine.replace(/^## (.*$)/, '

$1

'); } else if (processedLine.match(/^# /)) { processedLine = processedLine.replace(/^# (.*$)/, '

$1

'); } // Traiter les listes à puce if (processedLine.match(/^- /)) { if (!inUnorderedList) { processedLine = '' + processedLine; inUnorderedList = false; } if (inOrderedList) { processedLine = '' + processedLine; inOrderedList = false; } // Traiter les paragraphes if (processedLine.trim() === '') { processedLine = '

'; } else { processedLine = processedLine + '
'; } } processedLines.push(processedLine); } // Fermer les listes ouvertes à la fin if (inUnorderedList) { processedLines.push(''); } if (inOrderedList) { processedLines.push(''); } let htmlContent = processedLines.join('\n'); // Ajouter les balises de paragraphe si nécessaire if (!htmlContent.startsWith('${htmlContent}

`; } // 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 ''; // Diviser le contenu en lignes pour traiter les listes correctement const lines = content.split('\n'); const processedLines: string[] = []; let inUnorderedList = false; let inOrderedList = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Échapper les caractères HTML let processedLine = line .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // Traiter le markdown de base processedLine = processedLine .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/__(.*?)__/g, '$1') .replace(/~~(.*?)~~/g, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Traiter les titres if (processedLine.match(/^### /)) { processedLine = processedLine.replace(/^### (.*$)/, '

$1

'); } else if (processedLine.match(/^## /)) { processedLine = processedLine.replace(/^## (.*$)/, '

$1

'); } else if (processedLine.match(/^# /)) { processedLine = processedLine.replace(/^# (.*$)/, '

$1

'); } // Traiter les listes à puce if (processedLine.match(/^- /)) { if (!inUnorderedList) { processedLine = '' + processedLine; inUnorderedList = false; } if (inOrderedList) { processedLine = '' + processedLine; inOrderedList = false; } // Traiter les paragraphes if (processedLine.trim() === '') { processedLine = '

'; } else { processedLine = processedLine + '
'; } } processedLines.push(processedLine); } // Fermer les listes ouvertes à la fin if (inUnorderedList) { processedLines.push(''); } if (inOrderedList) { processedLines.push(''); } let result = processedLines.join('\n'); // Ajouter les balises de paragraphe si nécessaire if (!result.startsWith(''; } return result; } // 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 = [' { 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) + '...'; }