From d378aedd3c2a811c2baa56d070b77418e4abe0e4 Mon Sep 17 00:00:00 2001 From: Rahul Ranjan Date: Fri, 13 Nov 2026 07:12:03 +0000 Subject: [PATCH] fix: don't read past position 0 when checking dollar escaping in dollarmath is_escaped decremented its position index inside a loop whose condition allowed back_pos == 0, so on Python's negative indexing it read the last character of the inline source instead of finding no preceding character. A dollar at position 0 was then treated as escaped whenever the inline source ended with a backslash, silently dropping math at the start of a paragraph, and backslash runs before a mid-source dollar picked up trailing characters, flipping odd counts to even. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- mdit_py_plugins/dollarmath/index.py | 2 +- tests/test_dollarmath.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mdit_py_plugins/dollarmath/index.py b/mdit_py_plugins/dollarmath/index.py index c773c54..993da82 100644 --- a/mdit_py_plugins/dollarmath/index.py +++ b/mdit_py_plugins/dollarmath/index.py @@ -157,7 +157,7 @@ def is_escaped(state: StateInline, back_pos: int, mod: int = 0) -> bool: """Test if dollar is escaped.""" # count how many \ are before the current position backslashes = 0 - while back_pos >= 0: + while back_pos > 0: back_pos = back_pos - 1 if state.src[back_pos] == "\\": backslashes += 1 diff --git a/tests/test_dollarmath.py b/tests/test_dollarmath.py index 71c199e..5575f03 100644 --- a/tests/test_dollarmath.py +++ b/tests/test_dollarmath.py @@ -164,3 +164,38 @@ def test_label_is_html_escaped(label_normalizer, label): f'' "ΒΆ\na=1\n\n" ) + + +def test_inline_func_start_with_trailing_backslash(): + """A dollar at position 0 must not read backslashes from the *end* of the source. + + ``is_escaped`` used to decrement its position index past 0 and rely on + negative indexing, so it read the last character of the source instead of + finding no preceding character. Math at position 0 was then dropped + whenever the inline source ended with a backslash. + """ + inline_func = main.math_inline_dollar() + md = MarkdownIt() + for src in ("$a$ is math\\", "$a$ is math\\\\"): + tokens = [] + state = StateInline(src, md, {}, tokens) + assert inline_func(state, False) is True + assert tokens[0].type == "math_inline" + assert tokens[0].content == "a" + + +def test_inline_func_escaped_dollar_mid_source_with_trailing_backslash(): + """An escaped dollar mid-source must stay escaped even if the source ends with backslashes. + + With the same off-the-end read as the previous test, a backslash run + preceding the dollar could pick up the trailing character of the source + and turn an odd backslash count into an even one, wrongly treating an + escaped dollar as the opening delimiter of a math span. + """ + inline_func = main.math_inline_dollar() + md = MarkdownIt() + tokens = [] + state = StateInline("\\$x$ y\\", md, {}, tokens) + state.pos = 1 + assert inline_func(state, False) is False + assert tokens == []