| """ |
| Admonition extension for Python-Markdown |
| ======================================== |
| |
| Adds rST-style admonitions. Inspired by [rST][] feature with the same name. |
| |
| [rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions # noqa |
| |
| See <https://Python-Markdown.github.io/extensions/admonition> |
| for documentation. |
| |
| Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/). |
| |
| All changes Copyright The Python Markdown Project |
| |
| License: [BSD](https://opensource.org/licenses/bsd-license.php) |
| |
| """ |
| |
| from . import Extension |
| from ..blockprocessors import BlockProcessor |
| import xml.etree.ElementTree as etree |
| import re |
| |
| |
| class AdmonitionExtension(Extension): |
| """ Admonition extension for Python-Markdown. """ |
| |
| def extendMarkdown(self, md): |
| """ Add Admonition to Markdown instance. """ |
| md.registerExtension(self) |
| |
| md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105) |
| |
| |
| class AdmonitionProcessor(BlockProcessor): |
| |
| CLASSNAME = 'admonition' |
| CLASSNAME_TITLE = 'admonition-title' |
| RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)') |
| RE_SPACES = re.compile(' +') |
| |
| def __init__(self, parser): |
| """Initialization.""" |
| |
| super().__init__(parser) |
| |
| self.current_sibling = None |
| self.content_indention = 0 |
| |
| def parse_content(self, parent, block): |
| """Get sibling admontion. |
| |
| Retrieve the appropriate siblimg element. This can get trickly when |
| dealing with lists. |
| |
| """ |
| |
| old_block = block |
| the_rest = '' |
| |
| # We already acquired the block via test |
| if self.current_sibling is not None: |
| sibling = self.current_sibling |
| block, the_rest = self.detab(block, self.content_indent) |
| self.current_sibling = None |
| self.content_indent = 0 |
| return sibling, block, the_rest |
| |
| sibling = self.lastChild(parent) |
| |
| if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1: |
| sibling = None |
| else: |
| # If the last child is a list and the content is idented sufficient |
| # to be under it, then the content's is sibling is in the list. |
| last_child = self.lastChild(sibling) |
| indent = 0 |
| while last_child: |
| if ( |
| sibling and block.startswith(' ' * self.tab_length * 2) and |
| last_child and last_child.tag in ('ul', 'ol', 'dl') |
| ): |
| |
| # The expectation is that we'll find an <li> or <dt>. |
| # We should get it's last child as well. |
| sibling = self.lastChild(last_child) |
| last_child = self.lastChild(sibling) if sibling else None |
| |
| # Context has been lost at this point, so we must adjust the |
| # text's identation level so it will be evaluated correctly |
| # under the list. |
| block = block[self.tab_length:] |
| indent += self.tab_length |
| else: |
| last_child = None |
| |
| if not block.startswith(' ' * self.tab_length): |
| sibling = None |
| |
| if sibling is not None: |
| indent += self.tab_length |
| block, the_rest = self.detab(old_block, indent) |
| self.current_sibling = sibling |
| self.content_indent = indent |
| |
| return sibling, block, the_rest |
| |
| def test(self, parent, block): |
| |
| if self.RE.search(block): |
| return True |
| else: |
| return self.parse_content(parent, block)[0] is not None |
| |
| def run(self, parent, blocks): |
| block = blocks.pop(0) |
| m = self.RE.search(block) |
| |
| if m: |
| if m.start() > 0: |
| self.parser.parseBlocks(parent, [block[:m.start()]]) |
| block = block[m.end():] # removes the first line |
| block, theRest = self.detab(block) |
| else: |
| sibling, block, theRest = self.parse_content(parent, block) |
| |
| if m: |
| klass, title = self.get_class_and_title(m) |
| div = etree.SubElement(parent, 'div') |
| div.set('class', '{} {}'.format(self.CLASSNAME, klass)) |
| if title: |
| p = etree.SubElement(div, 'p') |
| p.text = title |
| p.set('class', self.CLASSNAME_TITLE) |
| else: |
| # Sibling is a list item, but we need to wrap it's content should be wrapped in <p> |
| if sibling.tag in ('li', 'dd') and sibling.text: |
| text = sibling.text |
| sibling.text = '' |
| p = etree.SubElement(sibling, 'p') |
| p.text = text |
| |
| div = sibling |
| |
| self.parser.parseChunk(div, block) |
| |
| if theRest: |
| # This block contained unindented line(s) after the first indented |
| # line. Insert these lines as the first block of the master blocks |
| # list for future processing. |
| blocks.insert(0, theRest) |
| |
| def get_class_and_title(self, match): |
| klass, title = match.group(1).lower(), match.group(2) |
| klass = self.RE_SPACES.sub(' ', klass) |
| if title is None: |
| # no title was provided, use the capitalized classname as title |
| # e.g.: `!!! note` will render |
| # `<p class="admonition-title">Note</p>` |
| title = klass.split(' ', 1)[0].capitalize() |
| elif title == '': |
| # an explicit blank title should not be rendered |
| # e.g.: `!!! warning ""` will *not* render `p` with a title |
| title = None |
| return klass, title |
| |
| |
| def makeExtension(**kwargs): # pragma: no cover |
| return AdmonitionExtension(**kwargs) |