/** Minimal markdown → HTML renderer for AI-generated text. */
function esc(s: string): string {
return s.replace(/&/g, '&').replace(//g, '>');
}
function inline(s: string): string {
return s
.replace(/`([^`\n]+)`/g, '$1')
.replace(/\*\*([^*\n]+)\*\*/g, '$1')
.replace(/\*([^*\n]+)\*/g, '$1');
}
export function markdownToHtml(text: string): string {
const codeBlocks: string[] = [];
// Extract fenced code blocks first
const withPlaceholders = text.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_m, _lang, code: string) => {
const i = codeBlocks.length;
codeBlocks.push(`
${esc(code.replace(/\n$/, ''))}`);
return `\x00CODE${i}\x00`;
});
const out: string[] = [];
const blocks = withPlaceholders.split(/\n{2,}/);
for (const block of blocks) {
const b = block.trim();
if (!b) continue;
// Code block placeholder (whole block)
const cp = b.match(/^\x00CODE(\d+)\x00$/);
if (cp) { out.push(codeBlocks[+cp[1]]); continue; }
// Heading
const h3 = b.match(/^###\s+(.+)/);
const h2 = b.match(/^##\s+(.+)/);
const h1 = b.match(/^#\s+(.+)/);
if (h3) { out.push(`${inline(esc(t))}
`); } } closeList(); out.push(parts.join('')); } return out.join(''); }