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