function escapeHtml(value) { return String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } function safeUrl(value) { const url = String(value).trim(); if (/^(https?:\/\/|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url); return "#"; } function inline(value) { const tokens = []; let html = escapeHtml(value); html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => { const token = `\u0000IMG${tokens.length}\u0000`; const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; tokens.push(`${alt}`); return token; }); html = html .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/~~([^~]+)~~/g, "$1") .replace(/\*([^*]+)\*/g, "$1") .replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => { const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; return `${label}`; }); return html.replace(/\u0000IMG(\d+)\u0000/g, (_, index) => tokens[Number(index)] || ""); } export function renderMarkdown(source) { let html = "", inCode = false, language = "", code = [], list = null; const closeList = () => { if (list) { html += ``; list = null; } }; const closeCode = () => { const body = escapeHtml(code.join("\n")); html += language.toLowerCase() === "mermaid" ? `
${body}
` : `
${body}
`; code = []; language = ""; }; for (const line of String(source).split("\n")) { if (line.startsWith("```")) { closeList(); if (inCode) closeCode(); else language = line.slice(3).trim(); inCode = !inCode; continue; } if (inCode) { code.push(line); continue; } const heading = line.match(/^(#{1,6})\s+(.+)$/), ul = line.match(/^\s*[-*+]\s+(.+)$/), ol = line.match(/^\s*\d+\.\s+(.+)$/); if (heading) { closeList(); const n = heading[1].length; html += `${inline(heading[2])}`; } else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } html += `
  • ${inline((ul || ol)[1])}
  • `; } else { closeList(); if (/^---+$/.test(line)) html += "
    "; else if (line.startsWith("> ")) html += `
    ${inline(line.slice(2))}
    `; else if (line.trim()) html += `

    ${inline(line)}

    `; else html += "
    "; } } closeList(); if (inCode) closeCode(); return html; }