summaryrefslogtreecommitdiff
path: root/src/mistune/directives/_fenced.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mistune/directives/_fenced.py')
-rw-r--r--src/mistune/directives/_fenced.py142
1 files changed, 142 insertions, 0 deletions
diff --git a/src/mistune/directives/_fenced.py b/src/mistune/directives/_fenced.py
new file mode 100644
index 0000000..818f130
--- /dev/null
+++ b/src/mistune/directives/_fenced.py
@@ -0,0 +1,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')