${codeRuns[+idx]}`);
+ };
+
+ // 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 = escapeHtml(tmp);
+
+ // 4) Headings
+ escaped = escaped
+ .replace(/^#### (.+)$/gm, "${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 => `
; keep raw \n (no
here!)
+ const escapedCode = escapeHtml(code);
+
+ // Single-line header to avoid global
interference
+ const encodedForCopy = encodeURIComponent(code);
+ const head = `${titleLabel}`;
+
+ // 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;
+}
+
+// Virtually close an unfinished fenced code block so it renders during streaming.
+function balanceStreamingCodeFence(md) {
+ // Split into lines; we only consider fences that start a line.
+ const lines = md.split(/\r?\n/);
+
+ // Track the last unmatched opening fence we see while scanning.
+ // { fenceChar: '`' or '~', fenceLen: number }
+ let open = null;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // Opening fence? ^\s*([`~]{3,})(.*)$
+ if (!open) {
+ const m = line.match(/^\s*([`~]{3,})([^\s]*)?.*$/);
+ if (m) {
+ // Treat as an opening fence
+ open = { fenceChar: m[1][0], fenceLen: m[1].length };
+ continue;
+ }
+ } else {
+ // Closing fence: must match same char and length or longer
+ const re = new RegExp(`^\\s*(${open.fenceChar}{${open.fenceLen},})\\s*$`);
+ if (re.test(line)) {
+ // Closed
+ open = null;
+ continue;
+ }
+ // Otherwise still inside the code block; keep scanning
+ }
+ }
+
+ if (open) {
+ // Virtually close with the same fence so the block renders now
+ const virtual = `${open.fenceChar.repeat(open.fenceLen)}`;
+ return md.endsWith('\n') ? md + virtual : md + '\n' + virtual;
+ }
+
+ return md;
+}