File: colon_fence.py

package info (click to toggle)
mdit-py-plugins 0.4.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 672 kB
  • sloc: python: 3,595; sh: 8; makefile: 7
file content (159 lines) | stat: -rw-r--r-- 3,955 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
156
157
158
159
from __future__ import annotations

from typing import TYPE_CHECKING, Sequence

from markdown_it import MarkdownIt
from markdown_it.common.utils import escapeHtml, unescapeAll
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


def colon_fence_plugin(md: MarkdownIt) -> None:
    """This plugin directly mimics regular fences, but with `:` colons.

    Example::

        :::name
        contained text
        :::

    """

    md.block.ruler.before(
        "fence",
        "colon_fence",
        _rule,
        {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
    )
    md.add_render_rule("colon_fence", _render)


def _rule(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
    if is_code_block(state, startLine):
        return False

    haveEndMarker = False
    pos = state.bMarks[startLine] + state.tShift[startLine]
    maximum = state.eMarks[startLine]

    if pos + 3 > maximum:
        return False

    marker = state.src[pos]

    if marker != ":":
        return False

    # scan marker length
    mem = pos
    pos = _skipCharsStr(state, pos, marker)

    length = pos - mem

    if length < 3:
        return False

    markup = state.src[mem:pos]
    params = state.src[pos:maximum]

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

    # search end of 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

        pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
        maximum = state.eMarks[nextLine]

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

        if state.src[pos] != marker:
            continue

        if is_code_block(state, nextLine):
            continue

        pos = _skipCharsStr(state, pos, marker)

        # closing code fence must be at least as long as the opening one
        if pos - mem < length:
            continue

        # make sure tail has spaces only
        pos = state.skipSpaces(pos)

        if pos < maximum:
            continue

        haveEndMarker = True
        # found!
        break

    # If a fence has heading spaces, they should be removed from its inner block
    length = state.sCount[startLine]

    state.line = nextLine + (1 if haveEndMarker else 0)

    token = state.push("colon_fence", "code", 0)
    token.info = params
    token.content = state.getLines(startLine + 1, nextLine, length, True)
    token.markup = markup
    token.map = [startLine, state.line]

    return True


def _skipCharsStr(state: StateBlock, pos: int, ch: str) -> int:
    """Skip character string from given position."""
    # TODO this can be replaced with StateBlock.skipCharsStr in markdown-it-py 3.0.0
    while True:
        try:
            current = state.src[pos]
        except IndexError:
            break
        if current != ch:
            break
        pos += 1
    return pos


def _render(
    self: RendererProtocol,
    tokens: Sequence[Token],
    idx: int,
    options: OptionsDict,
    env: EnvType,
) -> str:
    token = tokens[idx]
    info = unescapeAll(token.info).strip() if token.info else ""
    content = escapeHtml(token.content)
    block_name = ""

    if info:
        block_name = info.split()[0]

    return (
        "<pre><code"
        + (f' class="block-{block_name}" ' if block_name else "")
        + ">"
        + content
        + "</code></pre>\n"
    )