function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function sanitizeMarkdownUrl(url) { const s = String(url || "").trim(); if (!s) return ""; if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s)) { const scheme = s.split(":", 1)[0].toLowerCase(); if (!["http", "https", "mailto"].includes(scheme)) return ""; } return s; } function renderMarkdown(md) { const raw = String(md || ""); const escaped = escapeHtml(raw); const blocks = []; const placeholder = (i) => `@@BLOCK_${i}@@`; const fenced = escaped.replace(/```([\s\S]*?)```/g, (_m, code) => { const html = `
${code.replace(/^\n+|\n+$/g, "")}`;
blocks.push(html);
return placeholder(blocks.length - 1);
});
let html = fenced
.replace(/^### (.*)$/gm, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*\n]+)\*/g, "$1");
html = html.replace(/\n{2,}/g, "\n\n");
html = html
.split("\n\n")
.map((p) => {
if (p.startsWith("@@BLOCK_")) return p;
if (/^");
return `${lines}
`;
})
.join("\n");
blocks.forEach((b, i) => {
html = html.replaceAll(placeholder(i), b);
});
return html;
}