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