export function markdownToHTML(text) { // 0) Remove .../... blocks // This regex will match an an opening or tag, // followed by any characters (non-greedy), until either a closing // or tag is found, OR the end of the string ($). text = text.replace(/[\s\S]*?(?:<\/think(?:ing)?>|$)/gi, ''); // 1) Normalize line endings let tmp = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // 2) Extract code blocks and replace with placeholders (protect from all formatting) const codeblocks = []; const placeholder = idx => `@@CODEBLOCK${idx}@@`; tmp = tmp.replace(/```([^\n]*)\n([\s\S]*?)```/g, (_, lang, code) => { // Strip trailing whitespace-only lines at the end of the block let cleaned = (code || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const lines = cleaned.split('\n'); while (lines.length > 0 && /^\s*$/.test(lines[lines.length - 1])) lines.pop(); cleaned = lines.join('\n'); codeblocks.push({ lang: (lang || '').trim(), code: cleaned }); return placeholder(codeblocks.length - 1); }); // 3) HTML-escape special characters (outside of code blocks) let escaped = tmp .replace(/&/g, "&") .replace(//g, ">"); // 4) Headings escaped = escaped .replace(/^#### (.+)$/gm, "

$1

") .replace(/^### (.+)$/gm, "

$1

") .replace(/^## (.+)$/gm, "

$1

") .replace(/^# (.+)$/gm, "

$1

"); // 4.2) Remove blank empty lines immediately after headings escaped = escaped.replace( /(.*?<\/h[1-4]>)[ \t]*\n(?:[ \t]*\n)+/g, "$1\n" ); // 4.3) Blockquotes escaped = escaped.replace( /(^|\n)([ \t]*> .+(?:\n[ \t]*> .+)*)/g, (_, lead, blockquoteBlock) => { const lines = blockquoteBlock .split(/\n/) .map(line => line.replace(/^[ \t]*>\s*/, '').trim()) .join('\n'); return `${lead}
${lines}
`; } ); // 4.5) Unordered lists escaped = escaped.replace( /(^|\n)([ \t]*[-*] .+(?:\n[ \t]*[-*] .+)*)/g, (_, lead, listBlock) => { const items = listBlock .split(/\n/) .map(line => line.replace(/^[ \t]*[-*]\s+/, '').trim()) .map(item => `
  • ${item}
  • `) .join(''); return `${lead}`; } ); // 4.6) Markdown tables (GitHub-style). Strict: requires header, separator, ≥2 cols. const mdTableBlockRe = /(^\|[^\n]*\|?\s*\n\|\s*[:\-]+(?:\s*\|\s*[:\-]+)+\s*\|?\s*\n(?:\|[^\n]*\|?\s*(?:\n|$))*)/gm; escaped = escaped.replace(mdTableBlockRe, (block) => { const hadTrailingNewline = /\n$/.test(block); const lines = block.replace(/\n$/, '').split('\n'); const split = (line) => line.replace(/^\||\|$/g, '').split('|').map(s => s.trim()); const headers = split(lines[0]); const seps = split(lines[1]); if (headers.length < 2 || seps.length < 2) return block; if (!seps.every(s => /^[ :\-]+$/.test(s) && /-/.test(s))) return block; const aligns = seps.map(seg => { const s = seg.replace(/\s+/g,''); const left = s.startsWith(':'); const right = s.endsWith(':'); if (left && right) return 'center'; if (right) return 'right'; return 'left'; }); const bodyLines = lines.slice(2).filter(l => /^\|/.test(l.trim())); const cellStyle = (i) => ` style="text-align:${aligns[i] || 'left'};vertical-align:top;padding:.6rem .75rem"`; const ths = headers.map((h,i)=>`${h}`).join(''); const rows = bodyLines.map(line => { const cells = split(line); const tds = cells.map((c,i)=>`${c}`).join(''); return `${tds}`; }).join(''); const table = `${ths}${rows}
    `; return table + (hadTrailingNewline ? '\n' : ''); }); // 4.75) Horizontal rules escaped = escaped.replace(/^---\s*$/gm, "
    "); // 5) Bold, italic, inline code (inline code only; fenced were extracted) let html = escaped .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/(?$1") .replace(/`(.+?)`/g, "$1"); // 5.5) Links html = html.replace(/\[([^\]]+?)\]\(([^)]+?)\)/g, '$1 $2'); // 6) Convert line-breaks to
    for NON-code content (code is still placeholdered) html = html.replace(/\n/g, "
    "); // 6.1) Cleanup stray
    around lists/tables/wrappers that already exist html = html .replace(/
    \s*(
      )/g, "$1") .replace(/(<\/ul>)\s*
      /g, "$1") .replace(/
      \s*(
      ]*>)/g, "$1") .replace(/(<\/div>)\s*
      /g, "$1") .replace(/
      \s*(]*>)/g, "$1") .replace(/(<\/table>)\s*
      /g, "$1") .replace(/
      \s*(
      )/g, "$1") .replace(/(<\/blockquote>)\s*
      /g, "$1"); // 6.2) Trim spaces/tabs and remove empty newline(s) after
      , blockquote, ul html = html .replace(/(
      )[ \t]+/g, "$1") .replace(/(
      )(?:[ \t]*
      )+/g, "$1") .replace(/(<\/blockquote>)(?:[ \t]*
      )+/g, "$1") .replace(/(<\/ul>)(?:[ \t]*
      )+/g, "$1"); // 7) Restore code blocks with title bar (language) + copy button (no inline handlers) html = html.replace(/@@CODEBLOCK(\d+)@@/g, (_, idx) => { const { lang, code } = codeblocks[+idx]; const title = (lang && lang.trim()) ? lang.trim() : 'code'; // Escape only for HTML rendering inside ; keep raw \n (no
      here!) const escapedCode = code .replace(/&/g, "&") .replace(//g, ">"); // Single-line header to avoid global
      interference const head = `
      ${title}
      `; // Ensure wrapping inside container and preserve newlines for copy/paste const body = `
      ${escapedCode}
      `; return `
      ${head}${body}
      `; }); // 8) Final cleanup around codeblocks specifically (remove stray
      added next to placeholders) html = html .replace(/
      \s*(?=
      before opening .replace(/(
      ]*>[\s\S]*?<\/div>)\s*
      /g, "$1"); //
      right after closing return html; }