/** * Mini-renderer markdown sans dépendance externe. * * Volontairement minimal et stable : pas de plugins, pas d'extension HTML * arbitraire. Couvre les besoins des pages CMS Karbé (À propos, FAQ, CGV, * etc.) sans introduire de surface d'attaque XSS. * * Supporte : * # H1 / ## H2 / ### H3 * paragraphes (séparés par ligne vide) * **gras** et *italique* * [texte](https://lien) * - liste à puces * 1. liste numérotée * --- (séparateur) * > citation (blockquote) * * Toute autre balise HTML dans le markdown est échappée. */ function escapeHtml(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function inline(text: string): string { let out = escapeHtml(text); // **bold** out = out.replace(/\*\*([^*]+)\*\*/g, "$1"); // *italic* out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2"); // [text](url) out = out.replace( /\[([^\]]+)\]\(((?:https?:\/\/|\/|mailto:)[^)\s]+)\)/g, (_m, label: string, href: string) => { const safe = href.replace(/[&<>"']/g, (c) => `${c.charCodeAt(0)};`); const isExternal = /^https?:/.test(href); const extra = isExternal ? ' target="_blank" rel="noopener noreferrer"' : ""; return `${label}`; }, ); return out; } export function renderMarkdown(md: string): string { const lines = md.replace(/\r\n?/g, "\n").split("\n"); const out: string[] = []; let paragraph: string[] = []; let listType: "ul" | "ol" | null = null; let inBlockquote = false; const blockquote: string[] = []; function flushParagraph() { if (paragraph.length) { out.push(`
${inline(paragraph.join(" "))}
`); paragraph = []; } } function flushList() { if (listType) { out.push(`${listType}>`); listType = null; } } function flushBlockquote() { if (inBlockquote) { out.push( `${blockquote .map((l) => inline(l)) .join(" ")}`, ); blockquote.length = 0; inBlockquote = false; } } for (const rawLine of lines) { const line = rawLine.trimEnd(); if (!line.trim()) { flushParagraph(); flushList(); flushBlockquote(); continue; } if (/^---+$/.test(line.trim())) { flushParagraph(); flushList(); flushBlockquote(); out.push(`