Skip to content

Commit 4d7963d

Browse files
authored
Add amsmath extension (#12)
Adds extension to specifically capture top-level amsmath latex environments from: https://ctan.org/pkg/amsmath
1 parent e668abb commit 4d7963d

File tree

5 files changed

+501
-0
lines changed

5 files changed

+501
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""An extension to capture amsmath latex environments."""
2+
import re
3+
4+
from markdown_it import MarkdownIt
5+
from markdown_it.rules_block import StateBlock
6+
from markdown_it.common.utils import escapeHtml
7+
8+
# Taken from amsmath version 2.1
9+
# http://anorien.csc.warwick.ac.uk/mirrors/CTAN/macros/latex/required/amsmath/amsldoc.pdf
10+
ENVIRONMENTS = [
11+
# 3.2 single equation with an automatically gen-erated number
12+
"equation",
13+
# 3.3 variation equation, used for equations that don’t fit on a single line
14+
"multline",
15+
# 3.5 a group of consecutive equations when there is no alignment desired among them
16+
"gather",
17+
# 3.6 Used for two or more equations when vertical alignment is desired
18+
"align",
19+
# allows the horizontal space between equationsto be explicitly specified.
20+
"alignat",
21+
# stretches the space betweenthe equation columns to the maximum possible width
22+
"flalign",
23+
# 4.1 The pmatrix, bmatrix, Bmatrix, vmatrix and Vmatrix have (respectively)
24+
# (),[],{},||,and ‖‖ delimiters built in.
25+
"matrix",
26+
"pmatrix",
27+
"bmatrix",
28+
"Bmatrix",
29+
"vmatrix",
30+
"Vmatrix",
31+
# eqnarray is another math environment, it is not part of amsmath,
32+
# and note that it is better to use align or equation+split instead
33+
"eqnarray",
34+
]
35+
# other "non-top-level" environments:
36+
37+
# 3.4 the split environment is for single equations that are too long to fit on one line
38+
# and hence must be split into multiple lines,
39+
# it is intended for use only inside some other displayed equation structure,
40+
# usually an equation, align, or gather environment
41+
42+
# 3.7 variants gathered, aligned,and alignedat are provided
43+
# whose total width is the actual width of the contents;
44+
# thus they can be used as a component in a containing expression
45+
46+
RE_OPEN = re.compile(r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}")
47+
48+
49+
def amsmath_plugin(md: MarkdownIt):
50+
51+
md.block.ruler.before(
52+
"blockquote",
53+
"amsmath",
54+
amsmath_block,
55+
{"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
56+
)
57+
md.add_render_rule("amsmath", render_amsmath_block)
58+
59+
60+
def match_environment(string):
61+
match_open = RE_OPEN.match(string)
62+
if not match_open:
63+
return None
64+
environment = match_open.group(1)
65+
numbered = match_open.group(2)
66+
match_close = re.search(
67+
r"\\end\{" + environment + numbered.replace("*", r"\*") + "\\}", string
68+
)
69+
if not match_close:
70+
return None
71+
return (environment, numbered, match_close.end())
72+
73+
74+
def amsmath_block(state: StateBlock, startLine: int, endLine: int, silent: bool):
75+
76+
# if it's indented more than 3 spaces, it should be a code block
77+
if state.sCount[startLine] - state.blkIndent >= 4:
78+
return False
79+
80+
begin = state.bMarks[startLine] + state.tShift[startLine]
81+
82+
outcome = match_environment(state.src[begin:])
83+
if not outcome:
84+
return False
85+
environment, numbered, endpos = outcome
86+
endpos += begin
87+
88+
line = startLine
89+
while line < endLine:
90+
if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]:
91+
# line for end of block math found ...
92+
state.line = line + 1
93+
break
94+
line += 1
95+
96+
if not silent:
97+
token = state.push("amsmath", "math", 0)
98+
token.block = True
99+
token.content = state.src[begin:endpos]
100+
token.meta = {"environment": environment, "numbered": numbered}
101+
token.map = [startLine, line]
102+
103+
return True
104+
105+
106+
def render_amsmath_block(self, tokens, idx, options, env):
107+
token = tokens[idx]
108+
return (
109+
'<section class="amsmath">\n<eqn>\n'
110+
f"{escapeHtml(token.content)}\n</eqn>\n</section>\n"
111+
)

tests/test_amsmath/__init__.py

Whitespace-only changes.

tests/test_amsmath/fixtures.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
equation environment:
2+
.
3+
\begin{equation}
4+
a = 1
5+
\end{equation}
6+
.
7+
<section class="amsmath">
8+
<eqn>
9+
\begin{equation}
10+
a = 1
11+
\end{equation}
12+
</eqn>
13+
</section>
14+
.
15+
16+
equation* environment:
17+
.
18+
\begin{equation*}
19+
a = 1
20+
\end{equation*}
21+
.
22+
<section class="amsmath">
23+
<eqn>
24+
\begin{equation*}
25+
a = 1
26+
\end{equation*}
27+
</eqn>
28+
</section>
29+
.
30+
31+
multline environment:
32+
.
33+
\begin{multline}
34+
a = 1
35+
\end{multline}
36+
.
37+
<section class="amsmath">
38+
<eqn>
39+
\begin{multline}
40+
a = 1
41+
\end{multline}
42+
</eqn>
43+
</section>
44+
.
45+
46+
multline* environment:
47+
.
48+
\begin{multline*}
49+
a = 1
50+
\end{multline*}
51+
.
52+
<section class="amsmath">
53+
<eqn>
54+
\begin{multline*}
55+
a = 1
56+
\end{multline*}
57+
</eqn>
58+
</section>
59+
.
60+
61+
gather environment:
62+
.
63+
\begin{gather}
64+
a = 1
65+
\end{gather}
66+
.
67+
<section class="amsmath">
68+
<eqn>
69+
\begin{gather}
70+
a = 1
71+
\end{gather}
72+
</eqn>
73+
</section>
74+
.
75+
76+
gather* environment:
77+
.
78+
\begin{gather*}
79+
a = 1
80+
\end{gather*}
81+
.
82+
<section class="amsmath">
83+
<eqn>
84+
\begin{gather*}
85+
a = 1
86+
\end{gather*}
87+
</eqn>
88+
</section>
89+
.
90+
91+
align environment:
92+
.
93+
\begin{align}
94+
a = 1
95+
\end{align}
96+
.
97+
<section class="amsmath">
98+
<eqn>
99+
\begin{align}
100+
a = 1
101+
\end{align}
102+
</eqn>
103+
</section>
104+
.
105+
106+
align* environment:
107+
.
108+
\begin{align*}
109+
a = 1
110+
\end{align*}
111+
.
112+
<section class="amsmath">
113+
<eqn>
114+
\begin{align*}
115+
a = 1
116+
\end{align*}
117+
</eqn>
118+
</section>
119+
.
120+
121+
alignat environment:
122+
.
123+
\begin{alignat}
124+
a = 1
125+
\end{alignat}
126+
.
127+
<section class="amsmath">
128+
<eqn>
129+
\begin{alignat}
130+
a = 1
131+
\end{alignat}
132+
</eqn>
133+
</section>
134+
.
135+
136+
alignat* environment:
137+
.
138+
\begin{alignat*}
139+
a = 1
140+
\end{alignat*}
141+
.
142+
<section class="amsmath">
143+
<eqn>
144+
\begin{alignat*}
145+
a = 1
146+
\end{alignat*}
147+
</eqn>
148+
</section>
149+
.
150+
151+
flalign environment:
152+
.
153+
\begin{flalign}
154+
a = 1
155+
\end{flalign}
156+
.
157+
<section class="amsmath">
158+
<eqn>
159+
\begin{flalign}
160+
a = 1
161+
\end{flalign}
162+
</eqn>
163+
</section>
164+
.
165+
166+
flalign* environment:
167+
.
168+
\begin{flalign*}
169+
a = 1
170+
\end{flalign*}
171+
.
172+
<section class="amsmath">
173+
<eqn>
174+
\begin{flalign*}
175+
a = 1
176+
\end{flalign*}
177+
</eqn>
178+
</section>
179+
.
180+
181+
equation environment, with before/after paragraphs:
182+
.
183+
before
184+
\begin{equation}
185+
a = 1
186+
\end{equation}
187+
after
188+
.
189+
<p>before</p>
190+
<section class="amsmath">
191+
<eqn>
192+
\begin{equation}
193+
a = 1
194+
\end{equation}
195+
</eqn>
196+
</section>
197+
<p>after</p>
198+
.
199+
200+
equation environment, in list:
201+
.
202+
- \begin{equation}
203+
a = 1
204+
\end{equation}
205+
.
206+
<ul>
207+
<li>
208+
<section class="amsmath">
209+
<eqn>
210+
\begin{equation}
211+
a = 1
212+
\end{equation}
213+
</eqn>
214+
</section>
215+
</li>
216+
</ul>
217+
.

tests/test_amsmath/test_amsmath.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pathlib import Path
2+
from textwrap import dedent
3+
4+
import pytest
5+
6+
from markdown_it import MarkdownIt
7+
from markdown_it.extensions.amsmath import amsmath_plugin
8+
from markdown_it.utils import read_fixture_file
9+
10+
FIXTURE_PATH = Path(__file__).parent
11+
12+
13+
def test_plugin_parse(data_regression):
14+
md = MarkdownIt().use(amsmath_plugin)
15+
tokens = md.parse(
16+
dedent(
17+
"""\
18+
a
19+
\\begin{equation}
20+
b=1
21+
c=2
22+
\\end{equation}
23+
d
24+
"""
25+
)
26+
)
27+
data_regression.check([t.as_dict() for t in tokens])
28+
29+
30+
@pytest.mark.parametrize(
31+
"line,title,input,expected", read_fixture_file(FIXTURE_PATH.joinpath("fixtures.md"))
32+
)
33+
def test_fixtures(line, title, input, expected):
34+
md = MarkdownIt("commonmark").use(amsmath_plugin)
35+
md.options["xhtmlOut"] = False
36+
text = md.render(input)
37+
print(text)
38+
assert text.rstrip() == expected.rstrip()

0 commit comments

Comments
 (0)