${codeRuns[+idx]}`);
};
@@ -49,9 +48,9 @@ export function markdownToHTML(text) {
// 2) Extract code blocks and replace with placeholders (protect from all formatting)
const codeblocks = [];
- const placeholder = idx => `@@CODEBLOCK${idx}@@`;
+ 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();
@@ -60,15 +59,33 @@ export function markdownToHTML(text) {
return placeholder(codeblocks.length - 1);
});
- // 3) HTML-escape special characters (outside of code blocks)
+ // 3) HTML-escape special characters (outside of fenced code blocks)
let escaped = escapeHtml(tmp);
- // 4) Headings
+ // 4) Headings (with consistent hooks)
escaped = escaped
- .replace(/^#### (.+)$/gm, "${lines}`; + return `${lead}
${lines}`; } ); @@ -88,10 +105,12 @@ export function markdownToHTML(text) { (_, 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 head = `${titleLabel}`;
+
const body = `${escapedCode}
`;
return `${head}${body}`;
});
- // 8) Final cleanup around codeblocks specifically (remove stray
added next to placeholders)
+ // 8) Cleanup around codeblocks
html = html
- .replace(/
\s*(?= before opening
- .replace(/(]*>[\s\S]*?<\/div>)\s*
/g, "$1"); //
right after closing
+ .replace(/
\s*(?=]*>[\s\S]*?<\/div>)\s*
/g,
+ '$1'
+ );
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*$`);
+ 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;
-}
+}
\ No newline at end of file