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