1"""An extension to capture amsmath latex environments."""
2
3from __future__ import annotations
4
5import re
6from typing import TYPE_CHECKING, Callable, Sequence
7
8from markdown_it import MarkdownIt
9from markdown_it.common.utils import escapeHtml
10from markdown_it.rules_block import StateBlock
11
12from mdit_py_plugins.utils import is_code_block
13
14if TYPE_CHECKING:
15 from markdown_it.renderer import RendererProtocol
16 from markdown_it.token import Token
17 from markdown_it.utils import EnvType, OptionsDict
18
19# Taken from amsmath version 2.1
20# http://anorien.csc.warwick.ac.uk/mirrors/CTAN/macros/latex/required/amsmath/amsldoc.pdf
21ENVIRONMENTS = [
22 # 3.2 single equation with an automatically gen-erated number
23 "equation",
24 # 3.3 variation equation, used for equations that dont fit on a single line
25 "multline",
26 # 3.5 a group of consecutive equations when there is no alignment desired among them
27 "gather",
28 # 3.6 Used for two or more equations when vertical alignment is desired
29 "align",
30 # allows the horizontal space between equationsto be explicitly specified.
31 "alignat",
32 # stretches the space betweenthe equation columns to the maximum possible width
33 "flalign",
34 # 4.1 The pmatrix, bmatrix, Bmatrix, vmatrix and Vmatrix have (respectively)
35 # (),[],{},||,and ‖‖ delimiters built in.
36 "matrix",
37 "pmatrix",
38 "bmatrix",
39 "Bmatrix",
40 "vmatrix",
41 "Vmatrix",
42 # eqnarray is another math environment, it is not part of amsmath,
43 # and note that it is better to use align or equation+split instead
44 "eqnarray",
45]
46# other "non-top-level" environments:
47
48# 3.4 the split environment is for single equations that are too long to fit on one line
49# and hence must be split into multiple lines,
50# it is intended for use only inside some other displayed equation structure,
51# usually an equation, align, or gather environment
52
53# 3.7 variants gathered, aligned,and alignedat are provided
54# whose total width is the actual width of the contents;
55# thus they can be used as a component in a containing expression
56
57RE_OPEN = re.compile(r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}")
58
59
60def amsmath_plugin(
61 md: MarkdownIt, *, renderer: Callable[[str], str] | None = None
62) -> None:
63 """Parses TeX math equations, without any surrounding delimiters,
64 only for top-level `amsmath <https://ctan.org/pkg/amsmath>`__ environments:
65
66 .. code-block:: latex
67
68 \\begin{gather*}
69 a_1=b_1+c_1\\\\
70 a_2=b_2+c_2-d_2+e_2
71 \\end{gather*}
72
73 :param renderer: Function to render content, by default escapes HTML
74
75 """
76 md.block.ruler.before(
77 "blockquote",
78 "amsmath",
79 amsmath_block,
80 {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
81 )
82
83 _renderer = (lambda content: escapeHtml(content)) if renderer is None else renderer
84
85 def render_amsmath_block(
86 self: RendererProtocol,
87 tokens: Sequence[Token],
88 idx: int,
89 options: OptionsDict,
90 env: EnvType,
91 ) -> str:
92 content = _renderer(str(tokens[idx].content))
93 return f'<div class="math amsmath">\n{content}\n</div>\n'
94
95 md.add_render_rule("amsmath", render_amsmath_block)
96
97
98def 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
112def amsmath_block(
113 state: StateBlock, startLine: int, endLine: int, silent: bool
114) -> bool:
115 if is_code_block(state, startLine):
116 return False
117
118 begin = state.bMarks[startLine] + state.tShift[startLine]
119
120 outcome = match_environment(state.src[begin:])
121 if not outcome:
122 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
131 break
132 line += 1
133
134 if not silent:
135 token = state.push("amsmath", "math", 0)
136 token.block = True
137 token.content = state.src[begin:endpos]
138 token.meta = {"environment": environment, "numbered": numbered}
139 token.map = [startLine, line]
140
141 return True