summaryrefslogtreecommitdiffstats
path: root/mdit_py_plugins/container/index.py
blob: b6edd43aadab5bb825ed14a57caa12d9c80e43e1 (plain)
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""Process block-level custom containers."""
from math import floor
from typing import Callable, Optional

from markdown_it import MarkdownIt
from markdown_it.common.utils import charCodeAt
from markdown_it.rules_block import StateBlock


def container_plugin(
    md: MarkdownIt,
    name: str,
    marker: str = ":",
    validate: Optional[Callable[[str, str], bool]] = None,
    render=None,
):
    """Plugin ported from
    `markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__.

    It is a plugin for creating block-level custom containers:

    .. code-block:: md

        :::: name
        ::: name
        *markdown*
        :::
        ::::

    :param name: the name of the container to parse
    :param marker: the marker character to use
    :param validate: func(marker, param) -> bool, default matches against the name
    :param render: render func

    """

    def validateDefault(params: str, *args):
        return params.strip().split(" ", 2)[0] == name

    def renderDefault(self, tokens, idx, _options, env):
        # add a class to the opening tag
        if tokens[idx].nesting == 1:
            tokens[idx].attrJoin("class", name)

        return self.renderToken(tokens, idx, _options, env)

    min_markers = 3
    marker_str = marker
    marker_char = charCodeAt(marker_str, 0)
    marker_len = len(marker_str)
    validate = validate or validateDefault
    render = render or renderDefault

    def container_func(state: StateBlock, startLine: int, endLine: int, silent: bool):

        auto_closed = False
        start = state.bMarks[startLine] + state.tShift[startLine]
        maximum = state.eMarks[startLine]

        # Check out the first character quickly,
        # this should filter out most of non-containers
        if marker_char != state.srcCharCode[start]:
            return False

        # Check out the rest of the marker string
        pos = start + 1
        while pos <= maximum:
            try:
                character = state.src[pos]
            except IndexError:
                break
            if marker_str[(pos - start) % marker_len] != character:
                break
            pos += 1

        marker_count = floor((pos - start) / marker_len)
        if marker_count < min_markers:
            return False
        pos -= (pos - start) % marker_len

        markup = state.src[start:pos]
        params = state.src[pos:maximum]
        assert validate is not None
        if not validate(params, markup):
            return False

        # Since start is found, we can report success here in validation mode
        if silent:
            return True

        # Search for the end of the block
        nextLine = startLine

        while True:
            nextLine += 1
            if nextLine >= endLine:
                # unclosed block should be autoclosed by end of document.
                # also block seems to be autoclosed by end of parent
                break

            start = state.bMarks[nextLine] + state.tShift[nextLine]
            maximum = state.eMarks[nextLine]

            if start < maximum and state.sCount[nextLine] < state.blkIndent:
                # non-empty line with negative indent should stop the list:
                # - ```
                #  test
                break

            if marker_char != state.srcCharCode[start]:
                continue

            if state.sCount[nextLine] - state.blkIndent >= 4:
                # closing fence should be indented less than 4 spaces
                continue

            pos = start + 1
            while pos <= maximum:
                try:
                    character = state.src[pos]
                except IndexError:
                    break
                if marker_str[(pos - start) % marker_len] != character:
                    break
                pos += 1

            # closing code fence must be at least as long as the opening one
            if floor((pos - start) / marker_len) < marker_count:
                continue

            # make sure tail has spaces only
            pos -= (pos - start) % marker_len
            pos = state.skipSpaces(pos)

            if pos < maximum:
                continue

            # found!
            auto_closed = True
            break

        old_parent = state.parentType
        old_line_max = state.lineMax
        state.parentType = "container"

        # this will prevent lazy continuations from ever going past our end marker
        state.lineMax = nextLine

        token = state.push(f"container_{name}_open", "div", 1)
        token.markup = markup
        token.block = True
        token.info = params
        token.map = [startLine, nextLine]

        state.md.block.tokenize(state, startLine + 1, nextLine)

        token = state.push(f"container_{name}_close", "div", -1)
        token.markup = state.src[start:pos]
        token.block = True

        state.parentType = old_parent
        state.lineMax = old_line_max
        state.line = nextLine + (1 if auto_closed else 0)

        return True

    md.block.ruler.before(
        "fence",
        "container_" + name,
        container_func,
        {"alt": ["paragraph", "reference", "blockquote", "list"]},
    )
    md.add_render_rule(f"container_{name}_open", render)
    md.add_render_rule(f"container_{name}_close", render)