307 lines
9.3 KiB
TypeScript
307 lines
9.3 KiB
TypeScript
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, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
// Parser le markdown de base
|
|
processedLine = processedLine
|
|
.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
|
|
});
|
|
|
|
// Traiter les titres
|
|
if (processedLine.match(/^### /)) {
|
|
processedLine = processedLine.replace(/^### (.*$)/, '<h3>$1</h3>');
|
|
} else if (processedLine.match(/^## /)) {
|
|
processedLine = processedLine.replace(/^## (.*$)/, '<h2>$1</h2>');
|
|
} else if (processedLine.match(/^# /)) {
|
|
processedLine = processedLine.replace(/^# (.*$)/, '<h1>$1</h1>');
|
|
}
|
|
|
|
// Traiter les listes à puce
|
|
if (processedLine.match(/^- /)) {
|
|
if (!inUnorderedList) {
|
|
processedLine = '<ul>' + processedLine.replace(/^- (.*$)/, '<li>$1</li>');
|
|
inUnorderedList = true;
|
|
} else {
|
|
processedLine = processedLine.replace(/^- (.*$)/, '<li>$1</li>');
|
|
}
|
|
} else if (processedLine.match(/^\d+\. /)) {
|
|
if (!inOrderedList) {
|
|
processedLine = '<ol>' + processedLine.replace(/^\d+\. (.*$)/, '<li>$1</li>');
|
|
inOrderedList = true;
|
|
} else {
|
|
processedLine = processedLine.replace(/^\d+\. (.*$)/, '<li>$1</li>');
|
|
}
|
|
} else {
|
|
// Ligne normale - fermer les listes si nécessaire
|
|
if (inUnorderedList) {
|
|
processedLine = '</ul>' + processedLine;
|
|
inUnorderedList = false;
|
|
}
|
|
if (inOrderedList) {
|
|
processedLine = '</ol>' + processedLine;
|
|
inOrderedList = false;
|
|
}
|
|
|
|
// Traiter les paragraphes
|
|
if (processedLine.trim() === '') {
|
|
processedLine = '</p><p>';
|
|
} else {
|
|
processedLine = processedLine + '<br>';
|
|
}
|
|
}
|
|
|
|
processedLines.push(processedLine);
|
|
}
|
|
|
|
// Fermer les listes ouvertes à la fin
|
|
if (inUnorderedList) {
|
|
processedLines.push('</ul>');
|
|
}
|
|
if (inOrderedList) {
|
|
processedLines.push('</ol>');
|
|
}
|
|
|
|
let htmlContent = processedLines.join('\n');
|
|
|
|
// Ajouter les balises de paragraphe si nécessaire
|
|
if (!htmlContent.startsWith('<h') && !htmlContent.startsWith('<ul') && !htmlContent.startsWith('<ol')) {
|
|
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 '';
|
|
|
|
// 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, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
// Traiter le markdown de base
|
|
processedLine = processedLine
|
|
.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>');
|
|
|
|
// Traiter les titres
|
|
if (processedLine.match(/^### /)) {
|
|
processedLine = processedLine.replace(/^### (.*$)/, '<h3>$1</h3>');
|
|
} else if (processedLine.match(/^## /)) {
|
|
processedLine = processedLine.replace(/^## (.*$)/, '<h2>$1</h2>');
|
|
} else if (processedLine.match(/^# /)) {
|
|
processedLine = processedLine.replace(/^# (.*$)/, '<h1>$1</h1>');
|
|
}
|
|
|
|
// Traiter les listes à puce
|
|
if (processedLine.match(/^- /)) {
|
|
if (!inUnorderedList) {
|
|
processedLine = '<ul>' + processedLine.replace(/^- (.*$)/, '<li>$1</li>');
|
|
inUnorderedList = true;
|
|
} else {
|
|
processedLine = processedLine.replace(/^- (.*$)/, '<li>$1</li>');
|
|
}
|
|
} else if (processedLine.match(/^\d+\. /)) {
|
|
if (!inOrderedList) {
|
|
processedLine = '<ol>' + processedLine.replace(/^\d+\. (.*$)/, '<li>$1</li>');
|
|
inOrderedList = true;
|
|
} else {
|
|
processedLine = processedLine.replace(/^\d+\. (.*$)/, '<li>$1</li>');
|
|
}
|
|
} else {
|
|
// Ligne normale - fermer les listes si nécessaire
|
|
if (inUnorderedList) {
|
|
processedLine = '</ul>' + processedLine;
|
|
inUnorderedList = false;
|
|
}
|
|
if (inOrderedList) {
|
|
processedLine = '</ol>' + processedLine;
|
|
inOrderedList = false;
|
|
}
|
|
|
|
// Traiter les paragraphes
|
|
if (processedLine.trim() === '') {
|
|
processedLine = '</p><p>';
|
|
} else {
|
|
processedLine = processedLine + '<br>';
|
|
}
|
|
}
|
|
|
|
processedLines.push(processedLine);
|
|
}
|
|
|
|
// Fermer les listes ouvertes à la fin
|
|
if (inUnorderedList) {
|
|
processedLines.push('</ul>');
|
|
}
|
|
if (inOrderedList) {
|
|
processedLines.push('</ol>');
|
|
}
|
|
|
|
let result = processedLines.join('\n');
|
|
|
|
// Ajouter les balises de paragraphe si nécessaire
|
|
if (!result.startsWith('<h') && !result.startsWith('<ul') && !result.startsWith('<ol')) {
|
|
result = '<p>' + result + '</p>';
|
|
}
|
|
|
|
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 = ['<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) + '...';
|
|
}
|