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.replace(/^- (.*$)/, '- $1
');
inUnorderedList = true;
} else {
processedLine = processedLine.replace(/^- (.*$)/, '- $1
');
}
} else if (processedLine.match(/^\d+\. /)) {
if (!inOrderedList) {
processedLine = '' + processedLine.replace(/^\d+\. (.*$)/, '- $1
');
inOrderedList = true;
} else {
processedLine = processedLine.replace(/^\d+\. (.*$)/, '- $1
');
}
} else {
// Ligne normale - fermer les listes si nécessaire
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.replace(/^- (.*$)/, '- $1
');
inUnorderedList = true;
} else {
processedLine = processedLine.replace(/^- (.*$)/, '- $1
');
}
} else if (processedLine.match(/^\d+\. /)) {
if (!inOrderedList) {
processedLine = '' + processedLine.replace(/^\d+\. (.*$)/, '- $1
');
inOrderedList = true;
} else {
processedLine = processedLine.replace(/^\d+\. (.*$)/, '- $1
');
}
} else {
// Ligne normale - fermer les listes si nécessaire
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 = ['