File: __init__.py

package info (click to toggle)
mdit-py-plugins 0.5.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 692 kB
  • sloc: python: 3,702; sh: 8; makefile: 7
file content (155 lines) | stat: -rw-r--r-- 5,273 bytes parent folder | download
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