function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function escapeAttribute(value: string): string { return value.replaceAll("&", "&").replaceAll('"', """); } function normalizeHttpUrl(rawUrl: string): string | null { try { const parsed = new URL(rawUrl); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null; return parsed.toString(); } catch { return null; } } function renderInline(markdown: string): string { let rendered = escapeHtml(markdown); rendered = rendered.replace(/`([^`]+)`/g, (_match, value: string) => `${value}`); rendered = rendered.replace(/\*\*([^*]+)\*\*/g, (_match, value: string) => `${value}`); rendered = rendered.replace(/\*([^*]+)\*/g, (_match, value: string) => `${value}`); rendered = rendered.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label: string, url: string) => { const safeUrl = normalizeHttpUrl(url); if (!safeUrl) return label; return `${label}`; }); return rendered; } export function markdownToSafeHtml(markdown: string): string { const source = markdown.replace(/\r\n/g, "\n").trim(); if (!source) return "

Sin contenido.

"; const lines = source.split("\n"); const output: string[] = []; let listMode: "ol" | "ul" | null = null; const closeList = () => { if (!listMode) return; output.push(listMode === "ol" ? "" : ""); listMode = null; }; for (const rawLine of lines) { const line = rawLine.trim(); if (!line) { closeList(); continue; } const h3 = line.match(/^###\s+(.+)$/); if (h3) { closeList(); output.push(`

${renderInline(h3[1])}

`); continue; } const h2 = line.match(/^##\s+(.+)$/); if (h2) { closeList(); output.push(`

${renderInline(h2[1])}

`); continue; } const h1 = line.match(/^#\s+(.+)$/); if (h1) { closeList(); output.push(`

${renderInline(h1[1])}

`); continue; } const ordered = line.match(/^\d+\.\s+(.+)$/); if (ordered) { if (listMode !== "ol") { closeList(); output.push("
    "); listMode = "ol"; } output.push(`
  1. ${renderInline(ordered[1])}
  2. `); continue; } const unordered = line.match(/^[-*]\s+(.+)$/); if (unordered) { if (listMode !== "ul") { closeList(); output.push("