345 lines
11 KiB
Python
345 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Sequence
|
|
import re
|
|
from re import Match
|
|
from typing import TYPE_CHECKING, Any, TypedDict
|
|
|
|
from markdown_it import MarkdownIt
|
|
from markdown_it.common.utils import charCodeAt
|
|
|
|
if TYPE_CHECKING:
|
|
from markdown_it.renderer import RendererProtocol
|
|
from markdown_it.rules_block import StateBlock
|
|
from markdown_it.rules_inline import StateInline
|
|
from markdown_it.token import Token
|
|
from markdown_it.utils import EnvType, OptionsDict
|
|
|
|
|
|
def texmath_plugin(
|
|
md: MarkdownIt, delimiters: str = "dollars", macros: Any = None
|
|
) -> None:
|
|
"""Plugin ported from
|
|
`markdown-it-texmath <https://github.com/goessner/markdown-it-texmath>`__.
|
|
|
|
It parses TeX math equations set inside opening and closing delimiters:
|
|
|
|
.. code-block:: md
|
|
|
|
$\\alpha = \\frac{1}{2}$
|
|
|
|
:param delimiters: one of: brackets, dollars, gitlab, julia, kramdown
|
|
|
|
"""
|
|
macros = macros or {}
|
|
|
|
if delimiters in rules:
|
|
for rule_inline in rules[delimiters]["inline"]:
|
|
md.inline.ruler.before(
|
|
"escape", rule_inline["name"], make_inline_func(rule_inline)
|
|
)
|
|
|
|
def render_math_inline(
|
|
self: RendererProtocol,
|
|
tokens: Sequence[Token],
|
|
idx: int,
|
|
options: OptionsDict,
|
|
env: EnvType,
|
|
) -> str:
|
|
return rule_inline["tmpl"].format( # noqa: B023
|
|
render(tokens[idx].content, False, macros)
|
|
)
|
|
|
|
md.add_render_rule(rule_inline["name"], render_math_inline)
|
|
|
|
for rule_block in rules[delimiters]["block"]:
|
|
md.block.ruler.before(
|
|
"fence", rule_block["name"], make_block_func(rule_block)
|
|
)
|
|
|
|
def render_math_block(
|
|
self: RendererProtocol,
|
|
tokens: Sequence[Token],
|
|
idx: int,
|
|
options: OptionsDict,
|
|
env: EnvType,
|
|
) -> str:
|
|
return rule_block["tmpl"].format( # noqa: B023
|
|
render(tokens[idx].content, True, macros), tokens[idx].info
|
|
)
|
|
|
|
md.add_render_rule(rule_block["name"], render_math_block)
|
|
|
|
|
|
class _RuleDictReqType(TypedDict):
|
|
name: str
|
|
rex: re.Pattern[str]
|
|
tmpl: str
|
|
tag: str
|
|
|
|
|
|
class RuleDictType(_RuleDictReqType, total=False):
|
|
# Note in Python 3.10+ could use Req annotation
|
|
pre: Any
|
|
post: Any
|
|
|
|
|
|
def applyRule(
|
|
rule: RuleDictType, string: str, begin: int, inBlockquote: bool
|
|
) -> None | Match[str]:
|
|
if not (
|
|
string.startswith(rule["tag"], begin)
|
|
and (rule["pre"](string, begin) if "pre" in rule else True)
|
|
):
|
|
return None
|
|
|
|
match = rule["rex"].match(string[begin:])
|
|
|
|
if not match or match.start() != 0:
|
|
return None
|
|
|
|
lastIndex = match.end() + begin - 1
|
|
if "post" in rule and not (
|
|
rule["post"](string, lastIndex) # valid post-condition
|
|
# remove evil blockquote bug (https:#github.com/goessner/mdmath/issues/50)
|
|
and (not inBlockquote or "\n" not in match.group(1))
|
|
):
|
|
return None
|
|
return match
|
|
|
|
|
|
def make_inline_func(rule: RuleDictType) -> Callable[[StateInline, bool], bool]:
|
|
def _func(state: StateInline, silent: bool) -> bool:
|
|
res = applyRule(rule, state.src, state.pos, False)
|
|
if res:
|
|
if not silent:
|
|
token = state.push(rule["name"], "math", 0)
|
|
token.content = res[1] # group 1 from regex ..
|
|
token.markup = rule["tag"]
|
|
|
|
state.pos += res.end()
|
|
|
|
return bool(res)
|
|
|
|
return _func
|
|
|
|
|
|
def make_block_func(rule: RuleDictType) -> Callable[[StateBlock, int, int, bool], bool]:
|
|
def _func(state: StateBlock, begLine: int, endLine: int, silent: bool) -> bool:
|
|
begin = state.bMarks[begLine] + state.tShift[begLine]
|
|
res = applyRule(rule, state.src, begin, state.parentType == "blockquote")
|
|
if res:
|
|
if not silent:
|
|
token = state.push(rule["name"], "math", 0)
|
|
token.block = True
|
|
token.content = res[1]
|
|
token.info = res[len(res.groups())]
|
|
token.markup = rule["tag"]
|
|
|
|
line = begLine
|
|
endpos = begin + res.end() - 1
|
|
|
|
while line < endLine:
|
|
if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]:
|
|
# line for end of block math found ...
|
|
state.line = line + 1
|
|
break
|
|
line += 1
|
|
|
|
return bool(res)
|
|
|
|
return _func
|
|
|
|
|
|
def dollar_pre(src: str, beg: int) -> bool:
|
|
prv = charCodeAt(src[beg - 1], 0) if beg > 0 else False
|
|
return (
|
|
(not prv) or (prv != 0x5C and (prv < 0x30 or prv > 0x39)) # no backslash,
|
|
) # no decimal digit .. before opening '$'
|
|
|
|
|
|
def dollar_post(src: str, end: int) -> bool:
|
|
try:
|
|
nxt = src[end + 1] and charCodeAt(src[end + 1], 0)
|
|
except IndexError:
|
|
return True
|
|
return (
|
|
(not nxt) or (nxt < 0x30) or (nxt > 0x39)
|
|
) # no decimal digit .. after closing '$'
|
|
|
|
|
|
def render(tex: str, displayMode: bool, macros: Any) -> str:
|
|
return tex
|
|
# TODO better HTML renderer port for math
|
|
# try:
|
|
# res = katex.renderToString(tex,{throwOnError:False,displayMode,macros})
|
|
# except:
|
|
# res = tex+": "+err.message.replace("<","<")
|
|
# return res
|
|
|
|
|
|
# def use(katex): # math renderer used ...
|
|
# texmath.katex = katex; # ... katex solely at current ...
|
|
# return texmath;
|
|
# }
|
|
|
|
|
|
# All regexes areg global (g) and sticky (y), see:
|
|
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
|
|
|
|
|
|
rules: dict[str, dict[str, list[RuleDictType]]] = {
|
|
"brackets": {
|
|
"inline": [
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^\\\((.+?)\\\)", re.DOTALL),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "\\(",
|
|
}
|
|
],
|
|
"block": [
|
|
{
|
|
"name": "math_block_eqno",
|
|
"rex": re.compile(
|
|
r"^\\\[(((?!\\\]|\\\[)[\s\S])+?)\\\]\s*?\(([^)$\r\n]+?)\)", re.M
|
|
),
|
|
"tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
|
|
"tag": "\\[",
|
|
},
|
|
{
|
|
"name": "math_block",
|
|
"rex": re.compile(r"^\\\[([\s\S]+?)\\\]", re.M),
|
|
"tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
|
|
"tag": "\\[",
|
|
},
|
|
],
|
|
},
|
|
"gitlab": {
|
|
"inline": [
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^\$`(.+?)`\$"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$`",
|
|
}
|
|
],
|
|
"block": [
|
|
{
|
|
"name": "math_block_eqno",
|
|
"rex": re.compile(
|
|
r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
|
|
),
|
|
"tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n',
|
|
"tag": "```math",
|
|
},
|
|
{
|
|
"name": "math_block",
|
|
"rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
|
|
"tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
|
|
"tag": "```math",
|
|
},
|
|
],
|
|
},
|
|
"julia": {
|
|
"inline": [
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^`{2}([^`]+?)`{2}"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "``",
|
|
},
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^\$(\S[^$\r\n]*?[^\s\\]{1}?)\$"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$",
|
|
"pre": dollar_pre,
|
|
"post": dollar_post,
|
|
},
|
|
{
|
|
"name": "math_single",
|
|
"rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$",
|
|
"pre": dollar_pre,
|
|
"post": dollar_post,
|
|
},
|
|
],
|
|
"block": [
|
|
{
|
|
"name": "math_block_eqno",
|
|
"rex": re.compile(
|
|
r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
|
|
),
|
|
"tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
|
|
"tag": "```math",
|
|
},
|
|
{
|
|
"name": "math_block",
|
|
"rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
|
|
"tmpl": "<section><eqn>{0}</eqn></section>",
|
|
"tag": "```math",
|
|
},
|
|
],
|
|
},
|
|
"kramdown": {
|
|
"inline": [
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^\${2}([^$\r\n]*?)\${2}"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$$",
|
|
}
|
|
],
|
|
"block": [
|
|
{
|
|
"name": "math_block_eqno",
|
|
"rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
|
|
"tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
|
|
"tag": "$$",
|
|
},
|
|
{
|
|
"name": "math_block",
|
|
"rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
|
|
"tmpl": "<section><eqn>{0}</eqn></section>",
|
|
"tag": "$$",
|
|
},
|
|
],
|
|
},
|
|
"dollars": {
|
|
"inline": [
|
|
{
|
|
"name": "math_inline",
|
|
"rex": re.compile(r"^\$(\S[^$]*?[^\s\\]{1}?)\$"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$",
|
|
"pre": dollar_pre,
|
|
"post": dollar_post,
|
|
},
|
|
{
|
|
"name": "math_single",
|
|
"rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
|
|
"tmpl": "<eq>{0}</eq>",
|
|
"tag": "$",
|
|
"pre": dollar_pre,
|
|
"post": dollar_post,
|
|
},
|
|
],
|
|
"block": [
|
|
{
|
|
"name": "math_block_eqno",
|
|
"rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
|
|
"tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n',
|
|
"tag": "$$",
|
|
},
|
|
{
|
|
"name": "math_block",
|
|
"rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
|
|
"tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
|
|
"tag": "$$",
|
|
},
|
|
],
|
|
},
|
|
}
|