index.py 11 KB

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