function escapeHtml(input: string): string { return input .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatInline(input: string): string { let output = input; output = output.replace(/`([^`]+)`/g, "$1"); output = output.replace(/\*\*([^*]+)\*\*/g, "$1"); output = output.replace(/\*([^*]+)\*/g, "$1"); output = output.replace(/_([^_]+)_/g, "$1"); output = output.replace( /\[([^\]]+)\]\(([^)]+)\)/g, '$1', ); return output; } export function renderMarkdown(markdown: string): string { const source = markdown || ""; const codeBlocks: string[] = []; let content = source.replace( /```(?:[\w-]+)?\n([\s\S]*?)```/g, (_match, code) => { const escaped = escapeHtml(code.trimEnd()); const token = `@@CODEBLOCK_${codeBlocks.length}@@`; codeBlocks.push(`
${escaped}
`); return token; }, ); content = escapeHtml(content); const lines = content.split(/\r?\n/); const blocks: string[] = []; let inList = false; const closeList = () => { if (inList) { blocks.push(""); inList = false; } }; for (const rawLine of lines) { const line = rawLine.trim(); if (!line) { closeList(); continue; } const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); if (headingMatch) { closeList(); const level = headingMatch[1].length; blocks.push( `${formatInline(headingMatch[2])}`, ); continue; } const listMatch = line.match(/^[-*+]\s+(.*)$/); if (listMatch) { if (!inList) { blocks.push("