1# Admonition extension for Python-Markdown
2# ========================================
3
4# Adds rST-style admonitions. Inspired by [rST][] feature with the same name.
5
6# [rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions
7
8# See https://Python-Markdown.github.io/extensions/admonition
9# for documentation.
10
11# Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/).
12
13# All changes Copyright The Python Markdown Project
14
15# License: [BSD](https://opensource.org/licenses/bsd-license.php)
16
17
18"""
19Adds rST-style admonitions to Python-Markdown.
20Inspired by [rST][] feature with the same name.
21
22[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions
23
24See the [documentation](https://Python-Markdown.github.io/extensions/admonition)
25for details.
26"""
27
28from __future__ import annotations
29
30from . import Extension
31from ..blockprocessors import BlockProcessor
32import xml.etree.ElementTree as etree
33import re
34from typing import TYPE_CHECKING
35
36if TYPE_CHECKING: # pragma: no cover
37 from markdown import blockparser
38
39
40class AdmonitionExtension(Extension):
41 """ Admonition extension for Python-Markdown. """
42
43 def extendMarkdown(self, md):
44 """
45 Register the processor.
46
47 | Class Instance | Registry | Name | Priority |
48 | --------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------ | :------: |
49 | [`AdmonitionProcessor`][markdown.extensions.admonition.AdmonitionProcessor] | [`blockprocessors`][markdown.blockprocessors.build_block_parser] | `admonition` | `105` |
50
51 """
52 # flake8: noqa: E501 47-49
53 md.registerExtension(self)
54
55 md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105)
56
57
58class AdmonitionProcessor(BlockProcessor):
59
60 CLASSNAME = 'admonition'
61 CLASSNAME_TITLE = 'admonition-title'
62 RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
63 RE_SPACES = re.compile(' +')
64
65 def __init__(self, parser: blockparser.BlockParser):
66 """Initialization."""
67
68 super().__init__(parser)
69
70 self.current_sibling: etree.Element | None = None
71 self.content_indent = 0
72
73 def parse_content(self, parent: etree.Element, block: str) -> tuple[etree.Element | None, str, str]:
74 """Get sibling admonition.
75
76 Retrieve the appropriate sibling element. This can get tricky when
77 dealing with lists.
78
79 """
80
81 old_block = block
82 the_rest = ''
83
84 # We already acquired the block via test
85 if self.current_sibling is not None:
86 sibling = self.current_sibling
87 block, the_rest = self.detab(block, self.content_indent)
88 self.current_sibling = None
89 self.content_indent = 0
90 return sibling, block, the_rest
91
92 sibling = self.lastChild(parent)
93
94 if sibling is None or sibling.tag != 'div' or sibling.get('class', '').find(self.CLASSNAME) == -1:
95 sibling = None
96 else:
97 # If the last child is a list and the content is sufficiently indented
98 # to be under it, then the content's sibling is in the list.
99 last_child = self.lastChild(sibling)
100 indent = 0
101 while last_child is not None:
102 if (
103 sibling is not None and block.startswith(' ' * self.tab_length * 2) and
104 last_child is not None and last_child.tag in ('ul', 'ol', 'dl')
105 ):
106
107 # The expectation is that we'll find an `<li>` or `<dt>`.
108 # We should get its last child as well.
109 sibling = self.lastChild(last_child)
110 last_child = self.lastChild(sibling) if sibling is not None else None
111
112 # Context has been lost at this point, so we must adjust the
113 # text's indentation level so it will be evaluated correctly
114 # under the list.
115 block = block[self.tab_length:]
116 indent += self.tab_length
117 else:
118 last_child = None
119
120 if not block.startswith(' ' * self.tab_length):
121 sibling = None
122
123 if sibling is not None:
124 indent += self.tab_length
125 block, the_rest = self.detab(old_block, indent)
126 self.current_sibling = sibling
127 self.content_indent = indent
128
129 return sibling, block, the_rest
130
131 def test(self, parent: etree.Element, block: str) -> bool:
132
133 if self.RE.search(block):
134 return True
135 else:
136 return self.parse_content(parent, block)[0] is not None
137
138 def run(self, parent: etree.Element, blocks: list[str]) -> None:
139 block = blocks.pop(0)
140 m = self.RE.search(block)
141
142 if m:
143 if m.start() > 0:
144 self.parser.parseBlocks(parent, [block[:m.start()]])
145 block = block[m.end():] # removes the first line
146 block, theRest = self.detab(block)
147 else:
148 sibling, block, theRest = self.parse_content(parent, block)
149
150 if m:
151 klass, title = self.get_class_and_title(m)
152 div = etree.SubElement(parent, 'div')
153 div.set('class', '{} {}'.format(self.CLASSNAME, klass))
154 if title:
155 p = etree.SubElement(div, 'p')
156 p.text = title
157 p.set('class', self.CLASSNAME_TITLE)
158 else:
159 # Sibling is a list item, but we need to wrap it's content should be wrapped in <p>
160 if sibling.tag in ('li', 'dd') and sibling.text:
161 text = sibling.text
162 sibling.text = ''
163 p = etree.SubElement(sibling, 'p')
164 p.text = text
165
166 div = sibling
167
168 self.parser.parseChunk(div, block)
169
170 if theRest:
171 # This block contained unindented line(s) after the first indented
172 # line. Insert these lines as the first block of the master blocks
173 # list for future processing.
174 blocks.insert(0, theRest)
175
176 def get_class_and_title(self, match: re.Match[str]) -> tuple[str, str | None]:
177 klass, title = match.group(1).lower(), match.group(2)
178 klass = self.RE_SPACES.sub(' ', klass)
179 if title is None:
180 # no title was provided, use the capitalized class name as title
181 # e.g.: `!!! note` will render
182 # `<p class="admonition-title">Note</p>`
183 title = klass.split(' ', 1)[0].capitalize()
184 elif title == '':
185 # an explicit blank title should not be rendered
186 # e.g.: `!!! warning ""` will *not* render `p` with a title
187 title = None
188 return klass, title
189
190
191def makeExtension(**kwargs): # pragma: no cover
192 return AdmonitionExtension(**kwargs)