From b571c7ddc1429f5d489ec6e973c899f23c65124d Mon Sep 17 00:00:00 2001
From: mashallow <35926768+TuanKietTran@users.noreply.github.com>
Date: Sat, 11 Jul 2026 20:45:57 +0700
Subject: [PATCH] fix(markdown): stop currency dollars rendering as KaTeX
inline math (#5132)
---
static/js/markdown.js | 7 +++--
tests/test_markdown_rendering_js.py | 45 +++++++++++++++++++++++++++--
2 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/static/js/markdown.js b/static/js/markdown.js
index 439206fcb..8735b83e7 100644
--- a/static/js/markdown.js
+++ b/static/js/markdown.js
@@ -661,8 +661,11 @@ export function mdToHtml(src, opts) {
return placeholder;
} catch (e) { return match; }
});
- // Inline math: $...$ (not preceded/followed by $ or digit, not spanning multiple lines)
- s = s.replace(/(? {
+ // Inline math: $...$ — single line only, and Pandoc-style delimiter rules so
+ // currency doesn't render as math ("$5 to $10"): the opening $ must be
+ // immediately followed by a non-space, the closing $ must be immediately
+ // preceded by a non-space and not followed by a digit.
+ s = s.replace(/(? {
try {
const raw = math.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
diff --git a/tests/test_markdown_rendering_js.py b/tests/test_markdown_rendering_js.py
index e0f493eff..2ffe8914f 100644
--- a/tests/test_markdown_rendering_js.py
+++ b/tests/test_markdown_rendering_js.py
@@ -18,12 +18,24 @@ def node_available():
pytest.skip("node binary not on PATH")
-def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)"):
+def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)", with_katex: bool = False):
script = textwrap.dedent(
r"""
import fs from 'node:fs';
globalThis.window = { location: { origin: 'http://localhost' }, katex: null };
+ if (__WITH_KATEX__) {
+ // Minimal stand-in for the CDN katex global: wraps the source so tests
+ // can assert what was (or wasn't) handed to KaTeX.
+ const katexStub = {
+ renderToString(src, opts) {
+ const display = !!(opts && opts.displayMode);
+ return `${src}`;
+ },
+ };
+ globalThis.window.katex = katexStub;
+ globalThis.katex = katexStub;
+ }
globalThis.document = {
readyState: 'loading',
addEventListener() {},
@@ -77,7 +89,9 @@ def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)"):
const input = JSON.parse(process.argv[1]);
console.log(JSON.stringify({ html: __RENDER_EXPR__ }));
"""
- ).replace("__RENDER_EXPR__", render_expr)
+ ).replace("__RENDER_EXPR__", render_expr).replace(
+ "__WITH_KATEX__", "true" if with_katex else "false"
+ )
result = subprocess.run(
["node", "--input-type=module", "-e", script, json.dumps(markdown)],
cwd=_REPO,
@@ -200,6 +214,33 @@ def test_inline_code_content_is_html_escaped(node_available):
assert "" not in html
+def test_currency_dollar_amounts_are_not_rendered_as_math(node_available):
+ # "$5 to $10" used to pair the two dollar signs as inline-math delimiters
+ # and render "5 to" through KaTeX. Pandoc-style rules now reject it: the
+ # closing $ is preceded by a space and followed by a digit.
+ html = _run_markdown_case(
+ "The price rose from $5 to $10 overnight.", with_katex=True
+ )
+
+ assert 'class="katex"' not in html
+ assert "$5" in html
+ assert "$10" in html
+
+
+def test_inline_math_still_renders_through_katex(node_available):
+ html = _run_markdown_case("Pythagoras: $x^2 + y^2 = z^2$ holds.", with_katex=True)
+
+ assert 'x^2 + y^2 = z^2' in html
+ assert "$" not in html
+
+
+def test_display_math_still_renders_through_katex(node_available):
+ html = _run_markdown_case("$$\\frac{a}{b}$$", with_katex=True)
+
+ assert 'data-display="true"' in html
+ assert "$$" not in html
+
+
def test_dotted_python_import_paths_are_not_autolinked(node_available):
html = _run_markdown_case(
"from imblearn.combine import SMOTETomek\n"