summaryrefslogtreecommitdiff
path: root/src/mistune/directives/_fenced.py
blob: 818f130b2557652e0c9e51d5fd3aa8aa5e0525ac (plain) (blame)
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
import re
from ._base import DirectiveParser, BaseDirective

__all__ = ['FencedDirective']


_type_re = re.compile(r'^ *\{[a-zA-Z0-9_-]+\}')
_directive_re = re.compile(
    r'\{(?P<type>[a-zA-Z0-9_-]+)\} *(?P<title>[^\n]*)(?:\n|$)'
    r'(?P<options>(?:\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)'
    r'\n*(?P<text>(?:[^\n]*\n+)*)'
)


class FencedParser(DirectiveParser):
    name = 'fenced_directive'

    @staticmethod
    def parse_type(m: re.Match):
        return m.group('type')

    @staticmethod
    def parse_title(m: re.Match):
        return m.group('title')

    @staticmethod
    def parse_content(m: re.Match):
        return m.group('text')


class FencedDirective(BaseDirective):
    """A **fenced** style of directive looks like a fenced code block, it is
    inspired by markdown-it-docutils. The syntax looks like:

    .. code-block:: text

        ```{directive-type} title
        :option-key: option value
        :option-key: option value

        content text here
        ```

    To use ``FencedDirective``, developers can add it into plugin list in
    the :class:`Markdown` instance:

    .. code-block:: python

        import mistune
        from mistune.directives import FencedDirective, Admonition

        md = mistune.create_markdown(plugins=[
            # ...
            FencedDirective([Admonition()]),
        ])

    FencedDirective is using >= 3 backticks or curly-brackets for the fenced
    syntax. Developers can change it to other characters, e.g. colon:

    .. code-block:: python

            directive = FencedDirective([Admonition()], ':')

    And then the directive syntax would look like:

    .. code-block:: text

        ::::{note} Nesting directives
        You can nest directives by ensuring the start and end fence matching
        the length. For instance, in this example, the admonition is started
        with 4 colons, then it should end with 4 colons.

        You can nest another admonition with other length of colons except 4.

        :::{tip} Longer outermost fence
        It would be better that you put longer markers for the outer fence,
        and shorter markers for the inner fence. In this example, we put 4
        colons outsie, and 3 colons inside.
        :::
        ::::

    :param plugins: list of directive plugins
    :param markers: characters to determine the fence, default is backtick
                    and curly-bracket
    """
    parser = FencedParser

    def __init__(self, plugins, markers='`~'):
        super(FencedDirective, self).__init__(plugins)
        self.markers = markers
        _marker_pattern = '|'.join(re.escape(c) for c in markers)
        self.directive_pattern = (
            r'^(?P<fenced_directive_mark>(?:' + _marker_pattern + r'){3,})'
            r'\{[a-zA-Z0-9_-]+\}'
        )

    def _process_directive(self, block, marker, start, state):
        mlen = len(marker)
        cursor_start = start + len(marker)

        _end_pattern = (
            r'^ {0,3}' + marker[0] + '{' + str(mlen) + r',}'
            r'[ \t]*(?:\n|$)'
        )
        _end_re = re.compile(_end_pattern, re.M)

        _end_m = _end_re.search(state.src, cursor_start)
        if _end_m:
            text = state.src[cursor_start:_end_m.start()]
            end_pos = _end_m.end()
        else:
            text = state.src[cursor_start:]
            end_pos = state.cursor_max

        m = _directive_re.match(text)
        if not m:
            return

        self.parse_method(block, m, state)
        return end_pos

    def parse_directive(self, block, m, state):
        marker = m.group('fenced_directive_mark')
        return self._process_directive(block, marker, m.start(), state)

    def parse_fenced_code(self, block, m, state):
        info = m.group('fenced_3')
        if not info or not _type_re.match(info):
            return block.parse_fenced_code(m, state)

        if state.depth() >= block.max_nested_level:
            return block.parse_fenced_code(m, state)

        marker = m.group('fenced_2')
        return self._process_directive(block, marker, m.start(), state)

    def __call__(self, md):
        super(FencedDirective, self).__call__(md)
        if self.markers == '`~':
            md.block.register('fenced_code', None, self.parse_fenced_code)
        else:
            self.register_block_parser(md, 'fenced_code')