diff --git a/static/ui.js b/static/ui.js index 8eed5ab7..a7128bfc 100644 --- a/static/ui.js +++ b/static/ui.js @@ -81,12 +81,10 @@ function _renderUserFencedBlocks(text){ return `${esc(item.src)}`; }); let s=String(text||''); - // Stash math before escaping plain text; display delimiters must run before inline. - s=s.replace(/\$\$([\s\S]+?)\$\$/g,(_,m)=>stashMath('display',m)); - s=s.replace(/\\\[([\s\S]+?)\\\]/g,(_,m)=>stashMath('display',m)); - s=s.replace(/\$([^\s$\n][^$\n]*?[^\s$\n]|\S)\$/g,(_,m)=>stashMath('inline',m)); - s=s.replace(/\\\((.+?)\\\)/g,(_,m)=>stashMath('inline',m)); - // Extract fenced code blocks → stash, replace with null-token placeholder + // Extract fenced code blocks FIRST so math regexes never run inside fenced + // content. If math were stashed first, a user-typed code block containing + // \[..\] / \(..\) / $$..$$ would be rendered as a KaTeX block inside + //
instead of as literal source. Mirrors renderMd()'s ordering.
// CommonMark §4.5 line-anchored fence: the closing run must use at least
// as many backticks as the opener, so inner triple-backtick fences remain content.
s=s.replace(/(^|\n)[ ]{0,3}(`{3,})([^\n`]*)\n(?:([\s\S]*?)\n)?[ ]{0,3}\2`*[ \t]*(?=\n|$)/g,(_,lead,_fence,info,code)=>{
@@ -111,6 +109,12 @@ function _renderUserFencedBlocks(text){
}
return lead+'\x00UF'+(stash.length-1)+'\x00';
});
+ // Now stash math from the OUTSIDE-of-fence text. Display delimiters must
+ // run before inline so $$..$$ isn't mis-parsed as $..$..$..$.
+ s=s.replace(/\$\$([\s\S]+?)\$\$/g,(_,m)=>stashMath('display',m));
+ s=s.replace(/\\\[([\s\S]+?)\\\]/g,(_,m)=>stashMath('display',m));
+ s=s.replace(/\$([^\s$\n][^$\n]*?[^\s$\n]|\S)\$/g,(_,m)=>stashMath('inline',m));
+ s=s.replace(/\\\((.+?)\\\)/g,(_,m)=>stashMath('inline',m));
// Escape remaining plain text and convert newlines to
s=esc(s).replace(/\n/g,'
');
// Restore stashed code blocks, then math placeholders as KaTeX targets.
diff --git a/tests/test_issue347.py b/tests/test_issue347.py
index 756bcd20..4513139d 100644
--- a/tests/test_issue347.py
+++ b/tests/test_issue347.py
@@ -115,6 +115,41 @@ where \\(L_i(f)\\) = SPL at angle \\(i\\)."""
assert "\\(" not in html and "\\)" not in html, html
+def test_user_code_block_with_latex_syntax_renders_as_literal_code():
+ """User-bubble code blocks containing \\[..\\] / \\(..\\) / $$..$$ must
+ render as literal code source, not as KaTeX. _renderUserFencedBlocks
+ must stash code fences BEFORE math (mirroring renderMd's ordering); if
+ math is stashed first, a user-typed code block containing LaTeX-like
+ syntax gets a `` placeholder dropped INSIDE
+ ``, and the user's literal source is silently replaced by
+ rendered math.
+ """
+ sample = "```\n\\[ a + b \\] is wrong\n\\(L_i\\) too\n$$matrix$$\n```"
+ rendered = _run_renderers(sample)
+ user_html = rendered["user"]
+ # The whole code block should remain literal, no KaTeX wrappers inside.
+ assert "" in user_html, user_html
+ assert "katex-block" not in user_html, user_html
+ assert "katex-inline" not in user_html, user_html
+ # Backslashes survive HTML escape unchanged; the user's source is intact.
+ assert "\\[ a + b \\]" in user_html, user_html
+ assert "\\(L_i\\)" in user_html, user_html
+ assert "$$matrix$$" in user_html, user_html
+
+
+def test_user_bubble_top_level_latex_still_renders_after_fence_reorder():
+ """Sibling regression: top-level math (outside any code fence) must
+ still render through KaTeX in user bubbles after the fence-first
+ reorder. Guards against an over-correction that disables user-bubble
+ math rendering entirely.
+ """
+ sample = "math: \\[ x + y \\]\n\nand inline \\(L_i\\)"
+ rendered = _run_renderers(sample)
+ user_html = rendered["user"]
+ assert 'class="katex-block" data-katex="display"' in user_html, user_html
+ assert 'class="katex-inline" data-katex="inline"' in user_html, user_html
+
+
def test_katex_inline_placeholder_emitted():
"""renderMd restore pass must emit .katex-inline spans for inline math."""
assert 'katex-inline' in UI_JS, \