Skip to content

Commit 3f7fcc6

Browse files
authored
👌 Improve parsing of nested amsmath (#119)
The previous logic was problematic for amsmath blocks nested in other blocs (such as blockquotes) The new parsing code now principally follows the logic in `markdown_it/rules_block/fence.py` (see also https://spec.commonmark.org/0.30/#fenced-code-blocks), except that: 1. it allows for a closing tag on the same line as the opening tag, and 2. it does not allow for an opening tag without closing tag (i.e. no auto-closing)
1 parent 637f7e7 commit 3f7fcc6

File tree

2 files changed

+85
-33
lines changed

2 files changed

+85
-33
lines changed

‎mdit_py_plugins/amsmath/__init__.py

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
# whose total width is the actual width of the contents;
5555
# thus they can be used as a component in a containing expression
5656

57-
RE_OPEN = re.compile(r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}")
57+
RE_OPEN = r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}"
5858

5959

6060
def amsmath_plugin(
@@ -95,47 +95,60 @@ def render_amsmath_block(
9595
md.add_render_rule("amsmath", render_amsmath_block)
9696

9797

98-
def match_environment(string: str) -> None | tuple[str, str, int]:
99-
match_open = RE_OPEN.match(string)
100-
if not match_open:
101-
return None
102-
environment = match_open.group(1)
103-
numbered = match_open.group(2)
104-
match_close = re.search(
105-
r"\\end\{" + environment + numbered.replace("*", r"\*") + "\\}", string
106-
)
107-
if not match_close:
108-
return None
109-
return (environment, numbered, match_close.end())
110-
111-
11298
def amsmath_block(
11399
state: StateBlock, startLine: int, endLine: int, silent: bool
114100
) -> bool:
101+
# note the code principally follows the logic in markdown_it/rules_block/fence.py,
102+
# except that:
103+
# (a) it allows for closing tag on same line as opening tag
104+
# (b) it does not allow for opening tag without closing tag (i.e. no auto-closing)
105+
115106
if is_code_block(state, startLine):
116107
return False
117108

118-
begin = state.bMarks[startLine] + state.tShift[startLine]
109+
# does the first line contain the beginning of an amsmath environment
110+
first_start = state.bMarks[startLine] + state.tShift[startLine]
111+
first_end = state.eMarks[startLine]
112+
first_text = state.src[first_start:first_end]
119113

120-
outcome = match_environment(state.src[begin:])
121-
if not outcome:
114+
if not (match_open := re.match(RE_OPEN, first_text)):
122115
return False
123-
environment, numbered, endpos = outcome
124-
endpos += begin
125-
126-
line = startLine
127-
while line < endLine:
128-
if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]:
129-
# line for end of block math found ...
130-
state.line = line + 1
116+
117+
# construct the closing tag
118+
environment = match_open.group(1)
119+
numbered = match_open.group(2)
120+
closing = rf"\end{{{match_open.group(1)}{match_open.group(2)}}}"
121+
122+
# start looking for the closing tag, including the current line
123+
nextLine = startLine - 1
124+
125+
while True:
126+
nextLine += 1
127+
if nextLine >= endLine:
128+
# reached the end of the block without finding the closing tag
129+
return False
130+
131+
next_start = state.bMarks[nextLine] + state.tShift[nextLine]
132+
next_end = state.eMarks[nextLine]
133+
if next_start < first_end and state.sCount[nextLine] < state.blkIndent:
134+
# non-empty line with negative indent should stop the list:
135+
# - \begin{align}
136+
# test
137+
return False
138+
139+
if state.src[next_start:next_end].rstrip().endswith(closing):
140+
# found the closing tag
131141
break
132-
line += 1
142+
143+
state.line = nextLine + 1
133144

134145
if not silent:
135146
token = state.push("amsmath", "math", 0)
136147
token.block = True
137-
token.content = state.src[begin:endpos]
148+
token.content = state.getLines(
149+
startLine, state.line, state.sCount[startLine], False
150+
)
138151
token.meta = {"environment": environment, "numbered": numbered}
139-
token.map = [startLine, line]
152+
token.map = [startLine, nextLine]
140153

141154
return True

‎tests/fixtures/amsmath.md

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ a = 1
1111
</div>
1212
.
1313

14+
equation environment on one line:
15+
.
16+
\begin{equation}a = 1\end{equation}
17+
.
18+
<div class="math amsmath">
19+
\begin{equation}a = 1\end{equation}
20+
</div>
21+
.
22+
1423
equation* environment:
1524
.
1625
\begin{equation*}
@@ -181,13 +190,43 @@ equation environment, in list:
181190
<li>
182191
<div class="math amsmath">
183192
\begin{equation}
184-
a = 1
185-
\end{equation}
193+
a = 1
194+
\end{equation}
186195
</div>
187196
</li>
188197
</ul>
189198
.
190199

200+
equation environment, in block quote:
201+
.
202+
> \begin{matrix}
203+
> -0.707 & 0.408 & 0.577 \\
204+
> -0.707 & -0.408 & -0.577 \\
205+
> -0. & -0.816 & 0.577
206+
> \end{matrix}
207+
208+
> \begin{equation}
209+
a = 1
210+
\end{equation}
211+
.
212+
<blockquote>
213+
<div class="math amsmath">
214+
\begin{matrix}
215+
-0.707 &amp; 0.408 &amp; 0.577 \\
216+
-0.707 &amp; -0.408 &amp; -0.577 \\
217+
-0. &amp; -0.816 &amp; 0.577
218+
\end{matrix}
219+
</div>
220+
</blockquote>
221+
<blockquote>
222+
<div class="math amsmath">
223+
\begin{equation}
224+
a = 1
225+
\end{equation}
226+
</div>
227+
</blockquote>
228+
.
229+
191230
`alignat` environment and HTML escaping
192231
.
193232
\begin{alignat}{3}
@@ -242,7 +281,7 @@ Indented by 4 spaces, DISABLE-CODEBLOCKS
242281
.
243282
<div class="math amsmath">
244283
\begin{equation}
245-
a = 1
246-
\end{equation}
284+
a = 1
285+
\end{equation}
247286
</div>
248287
.

0 commit comments

Comments
 (0)