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(``);
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}>`; list = null; } };
const closeCode = () => {
const body = escapeHtml(code.join("\n"));
html += language.toLowerCase() === "mermaid"
? `
${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(line.slice(2))}`; else if (line.trim()) html += `
${inline(line)}
`; else html += "