1from __future__ import annotations
2
3import re
4from typing import TYPE_CHECKING, Any, Callable, Match, Sequence, TypedDict
5
6from markdown_it import MarkdownIt
7from markdown_it.common.utils import charCodeAt
8
9if TYPE_CHECKING:
10 from markdown_it.renderer import RendererProtocol
11 from markdown_it.rules_block import StateBlock
12 from markdown_it.rules_inline import StateInline
13 from markdown_it.token import Token
14 from markdown_it.utils import EnvType, OptionsDict
15
16
17def texmath_plugin(
18 md: MarkdownIt, delimiters: str = "dollars", macros: Any = None
19) -> None:
20 """Plugin ported from
21 `markdown-it-texmath <https://github.com/goessner/markdown-it-texmath>`__.
22
23 It parses TeX math equations set inside opening and closing delimiters:
24
25 .. code-block:: md
26
27 $\\alpha = \\frac{1}{2}$
28
29 :param delimiters: one of: brackets, dollars, gitlab, julia, kramdown
30
31 """
32 macros = macros or {}
33
34 if delimiters in rules:
35 for rule_inline in rules[delimiters]["inline"]:
36 md.inline.ruler.before(
37 "escape", rule_inline["name"], make_inline_func(rule_inline)
38 )
39
40 def render_math_inline(
41 self: RendererProtocol,
42 tokens: Sequence[Token],
43 idx: int,
44 options: OptionsDict,
45 env: EnvType,
46 ) -> str:
47 return rule_inline["tmpl"].format( # noqa: B023
48 render(tokens[idx].content, False, macros)
49 )
50
51 md.add_render_rule(rule_inline["name"], render_math_inline)
52
53 for rule_block in rules[delimiters]["block"]:
54 md.block.ruler.before(
55 "fence", rule_block["name"], make_block_func(rule_block)
56 )
57
58 def render_math_block(
59 self: RendererProtocol,
60 tokens: Sequence[Token],
61 idx: int,
62 options: OptionsDict,
63 env: EnvType,
64 ) -> str:
65 return rule_block["tmpl"].format( # noqa: B023
66 render(tokens[idx].content, True, macros), tokens[idx].info
67 )
68
69 md.add_render_rule(rule_block["name"], render_math_block)
70
71
72class _RuleDictReqType(TypedDict):
73 name: str
74 rex: re.Pattern[str]
75 tmpl: str
76 tag: str
77
78
79class RuleDictType(_RuleDictReqType, total=False):
80 # Note in Python 3.10+ could use Req annotation
81 pre: Any
82 post: Any
83
84
85def applyRule(
86 rule: RuleDictType, string: str, begin: int, inBlockquote: bool
87) -> None | Match[str]:
88 if not (
89 string.startswith(rule["tag"], begin)
90 and (rule["pre"](string, begin) if "pre" in rule else True)
91 ):
92 return None
93
94 match = rule["rex"].match(string[begin:])
95
96 if not match or match.start() != 0:
97 return None
98
99 lastIndex = match.end() + begin - 1
100 if "post" in rule and not (
101 rule["post"](string, lastIndex) # valid post-condition
102 # remove evil blockquote bug (https:#github.com/goessner/mdmath/issues/50)
103 and (not inBlockquote or "\n" not in match.group(1))
104 ):
105 return None
106 return match
107
108
109def make_inline_func(rule: RuleDictType) -> Callable[[StateInline, bool], bool]:
110 def _func(state: StateInline, silent: bool) -> bool:
111 res = applyRule(rule, state.src, state.pos, False)
112 if res:
113 if not silent:
114 token = state.push(rule["name"], "math", 0)
115 token.content = res[1] # group 1 from regex ..
116 token.markup = rule["tag"]
117
118 state.pos += res.end()
119
120 return bool(res)
121
122 return _func
123
124
125def make_block_func(rule: RuleDictType) -> Callable[[StateBlock, int, int, bool], bool]:
126 def _func(state: StateBlock, begLine: int, endLine: int, silent: bool) -> bool:
127 begin = state.bMarks[begLine] + state.tShift[begLine]
128 res = applyRule(rule, state.src, begin, state.parentType == "blockquote")
129 if res:
130 if not silent:
131 token = state.push(rule["name"], "math", 0)
132 token.block = True
133 token.content = res[1]
134 token.info = res[len(res.groups())]
135 token.markup = rule["tag"]
136
137 line = begLine
138 endpos = begin + res.end() - 1
139
140 while line < endLine:
141 if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]:
142 # line for end of block math found ...
143 state.line = line + 1
144 break
145 line += 1
146
147 return bool(res)
148
149 return _func
150
151
152def dollar_pre(src: str, beg: int) -> bool:
153 prv = charCodeAt(src[beg - 1], 0) if beg > 0 else False
154 return (
155 (not prv) or prv != 0x5C and (prv < 0x30 or prv > 0x39) # no backslash,
156 ) # no decimal digit .. before opening '$'
157
158
159def dollar_post(src: str, end: int) -> bool:
160 try:
161 nxt = src[end + 1] and charCodeAt(src[end + 1], 0)
162 except IndexError:
163 return True
164 return (
165 (not nxt) or (nxt < 0x30) or (nxt > 0x39)
166 ) # no decimal digit .. after closing '$'
167
168
169def render(tex: str, displayMode: bool, macros: Any) -> str:
170 return tex
171 # TODO better HTML renderer port for math
172 # try:
173 # res = katex.renderToString(tex,{throwOnError:False,displayMode,macros})
174 # except:
175 # res = tex+": "+err.message.replace("<","<")
176 # return res
177
178
179# def use(katex): # math renderer used ...
180# texmath.katex = katex; # ... katex solely at current ...
181# return texmath;
182# }
183
184
185# All regexes areg global (g) and sticky (y), see:
186# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
187
188
189rules: dict[str, dict[str, list[RuleDictType]]] = {
190 "brackets": {
191 "inline": [
192 {
193 "name": "math_inline",
194 "rex": re.compile(r"^\\\((.+?)\\\)", re.DOTALL),
195 "tmpl": "<eq>{0}</eq>",
196 "tag": "\\(",
197 }
198 ],
199 "block": [
200 {
201 "name": "math_block_eqno",
202 "rex": re.compile(
203 r"^\\\[(((?!\\\]|\\\[)[\s\S])+?)\\\]\s*?\(([^)$\r\n]+?)\)", re.M
204 ),
205 "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
206 "tag": "\\[",
207 },
208 {
209 "name": "math_block",
210 "rex": re.compile(r"^\\\[([\s\S]+?)\\\]", re.M),
211 "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
212 "tag": "\\[",
213 },
214 ],
215 },
216 "gitlab": {
217 "inline": [
218 {
219 "name": "math_inline",
220 "rex": re.compile(r"^\$`(.+?)`\$"),
221 "tmpl": "<eq>{0}</eq>",
222 "tag": "$`",
223 }
224 ],
225 "block": [
226 {
227 "name": "math_block_eqno",
228 "rex": re.compile(
229 r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
230 ),
231 "tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n',
232 "tag": "```math",
233 },
234 {
235 "name": "math_block",
236 "rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
237 "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
238 "tag": "```math",
239 },
240 ],
241 },
242 "julia": {
243 "inline": [
244 {
245 "name": "math_inline",
246 "rex": re.compile(r"^`{2}([^`]+?)`{2}"),
247 "tmpl": "<eq>{0}</eq>",
248 "tag": "``",
249 },
250 {
251 "name": "math_inline",
252 "rex": re.compile(r"^\$(\S[^$\r\n]*?[^\s\\]{1}?)\$"),
253 "tmpl": "<eq>{0}</eq>",
254 "tag": "$",
255 "pre": dollar_pre,
256 "post": dollar_post,
257 },
258 {
259 "name": "math_single",
260 "rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
261 "tmpl": "<eq>{0}</eq>",
262 "tag": "$",
263 "pre": dollar_pre,
264 "post": dollar_post,
265 },
266 ],
267 "block": [
268 {
269 "name": "math_block_eqno",
270 "rex": re.compile(
271 r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
272 ),
273 "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
274 "tag": "```math",
275 },
276 {
277 "name": "math_block",
278 "rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
279 "tmpl": "<section><eqn>{0}</eqn></section>",
280 "tag": "```math",
281 },
282 ],
283 },
284 "kramdown": {
285 "inline": [
286 {
287 "name": "math_inline",
288 "rex": re.compile(r"^\${2}([^$\r\n]*?)\${2}"),
289 "tmpl": "<eq>{0}</eq>",
290 "tag": "$$",
291 }
292 ],
293 "block": [
294 {
295 "name": "math_block_eqno",
296 "rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
297 "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
298 "tag": "$$",
299 },
300 {
301 "name": "math_block",
302 "rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
303 "tmpl": "<section><eqn>{0}</eqn></section>",
304 "tag": "$$",
305 },
306 ],
307 },
308 "dollars": {
309 "inline": [
310 {
311 "name": "math_inline",
312 "rex": re.compile(r"^\$(\S[^$]*?[^\s\\]{1}?)\$"),
313 "tmpl": "<eq>{0}</eq>",
314 "tag": "$",
315 "pre": dollar_pre,
316 "post": dollar_post,
317 },
318 {
319 "name": "math_single",
320 "rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
321 "tmpl": "<eq>{0}</eq>",
322 "tag": "$",
323 "pre": dollar_pre,
324 "post": dollar_post,
325 },
326 ],
327 "block": [
328 {
329 "name": "math_block_eqno",
330 "rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
331 "tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n',
332 "tag": "$$",
333 },
334 {
335 "name": "math_block",
336 "rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
337 "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
338 "tag": "$$",
339 },
340 ],
341 },
342}