diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b78b3..fb82c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## Unreleased + +- 🐛 FIX: `texmath` no longer loops forever on block math opened inside a blockquote and closed after it (#156) + + The block rules matched their regex against the rest of the source, so the closing + delimiter could lie outside the current block; the rule then reported a match + without advancing `state.line`. The closing line is now searched within the current + block only, otherwise the rule does not match (no auto-closing, as in `amsmath`). + ## 0.7.0 - 2026-07-19 - ✨ NEW: Add section reference plugin (`section_ref`) (#144) diff --git a/mdit_py_plugins/texmath/index.py b/mdit_py_plugins/texmath/index.py index 67372fa..5f54f91 100644 --- a/mdit_py_plugins/texmath/index.py +++ b/mdit_py_plugins/texmath/index.py @@ -128,25 +128,28 @@ def make_block_func(rule: RuleDictType) -> Callable[[StateBlock, int, int, bool] def _func(state: StateBlock, begLine: int, endLine: int, silent: bool) -> bool: begin = state.bMarks[begLine] + state.tShift[begLine] res = applyRule(rule, state.src, begin, state.parentType == "blockquote") - if res: - if not silent: - token = state.push(rule["name"], "math", 0) - token.block = True - token.content = res[1] - token.info = res[len(res.groups())] - token.markup = rule["tag"] - - line = begLine - endpos = begin + res.end() - 1 - - while line < endLine: - if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]: - # line for end of block math found ... - state.line = line + 1 - break - line += 1 - - return bool(res) + if not res: + return False + + # find the line holding the end of the match, within this block only + endpos = begin + res.end() - 1 + line = begLine + while line < endLine and endpos > state.eMarks[line]: + line += 1 + if line >= endLine or endpos < state.bMarks[line]: + # the closing delimiter lies outside the current block, + # e.g. math opened inside a blockquote and closed after it + return False + + if not silent: + token = state.push(rule["name"], "math", 0) + token.block = True + token.content = res[1] + token.info = res[len(res.groups())] + token.markup = rule["tag"] + + state.line = line + 1 + return True return _func diff --git a/tests/fixtures/texmath_bracket.md b/tests/fixtures/texmath_bracket.md index 2f00e07..97b3b7d 100644 --- a/tests/fixtures/texmath_bracket.md +++ b/tests/fixtures/texmath_bracket.md @@ -410,3 +410,15 @@ Indented by 4 spaces, DISABLE-CODEBLOCKS . + +display equation opened in blockquote and closed outside it (valid=False) +. +> \[ a=1 + +\] +. +
+

[ a=1

+
+

]

+. diff --git a/tests/fixtures/texmath_dollar.md b/tests/fixtures/texmath_dollar.md index 26357b3..d7d8936 100644 --- a/tests/fixtures/texmath_dollar.md +++ b/tests/fixtures/texmath_dollar.md @@ -454,3 +454,31 @@ Indented by 4 spaces, DISABLE-CODEBLOCKS a . + +display equation opened in blockquote and closed outside it (valid=False) +. +> $$ a=1 + +$$ +. +
+

$$ a=1

+
+

$$

+. + +display equation opened in nested blockquote and closed outside it (valid=False) +. +- > $$ a=1 + +$$ (1) +. + +

$$ (1)

+. diff --git a/tests/test_texmath.py b/tests/test_texmath.py index 42f7d9b..3739150 100644 --- a/tests/test_texmath.py +++ b/tests/test_texmath.py @@ -80,6 +80,23 @@ def test_plugin_parse(data_regression): data_regression.check([t.as_dict() for t in tokens]) +@pytest.mark.parametrize( + "delimiters,opening,closing", + [ + ("brackets", "\\[", "\\]"), + ("dollars", "$$", "$$"), + ("kramdown", "$$", "$$"), + ("gitlab", "```math", "```"), + ("julia", "```math", "```"), + ], +) +def test_block_closed_outside_blockquote(delimiters, opening, closing): + """A math block opened inside a blockquote cannot close after it.""" + md = MarkdownIt("commonmark").use(texmath_plugin, delimiters=delimiters) + tokens = md.parse(f"> {opening} a=1\n\n{closing}\n") + assert not [t for t in tokens if t.type.startswith("math_block")] + + @pytest.mark.parametrize( "line,title,input,expected", read_fixture_file(FIXTURE_PATH.joinpath("texmath_dollar.md")),