function escapeHtml(value) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return String(value || "").replace(/[&<>"']/g, (char) => map[char]);
}
function safeUrl(value) {
const raw = String(value || "").trim();
if (!raw) return "";
if (/^(https?:\/\/|\/(?!\/)|#)/i.test(raw)) return escapeHtml(raw);
return "";
}
function renderMarkdownInline(value) {
let output = escapeHtml(value);
output = output.replace(/`([^`]+)`/g, "$1");
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
const href = safeUrl(url);
return href ? `${escapeHtml(text)}` : escapeHtml(text);
});
output = output.replace(/\*\*([^*]+)\*\*/g, "$1");
output = output.replace(/\*([^*]+)\*/g, "$1");
return output;
}
function renderMarkdown(value) {
const lines = String(value || "").replace(/\r\n?/g, "\n").split("\n");
let html = "";
let paragraph = [];
let listType = null;
let inCode = false;
let codeLang = "";
let codeLines = [];
const flushParagraph = () => {
if (!paragraph.length) return;
html += `
${renderMarkdownInline(paragraph.join(" "))}
`; paragraph = []; }; const closeList = () => { if (!listType) return; html += `${listType}>`; listType = null; }; for (const line of lines) { const trimmed = line.trim(); if (inCode) { if (trimmed.startsWith("```")) { const langClass = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : ""; html += `${escapeHtml(codeLines.join("\n"))}`;
inCode = false;
codeLang = "";
codeLines = [];
continue;
}
codeLines.push(line);
continue;
}
if (trimmed.startsWith("```")) {
flushParagraph();
closeList();
inCode = true;
codeLang = trimmed.slice(3).trim().replace(/[^a-z0-9_-]/gi, "").slice(0, 32);
continue;
}
if (!trimmed) {
flushParagraph();
closeList();
continue;
}
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
flushParagraph();
closeList();
const level = heading[1].length;
html += `${escapeHtml(codeLines.join("\n"))}`;
}
flushParagraph();
closeList();
return html || "";
}
module.exports = {
escapeHtml,
renderMarkdown,
renderMarkdownInline,
safeUrl
};