# 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 # 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) """ Adds rST-style admonitions. Inspired by [rST][] feature with the same name. [rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions See the [documentation](https://Python-Markdown.github.io/extensions/admonition) for details. """ from __future__ import annotations from . import Extension from ..blockprocessors import BlockProcessor import xml.etree.ElementTree as etree import re from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover from markdown import blockparser 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: blockparser.BlockParser): """Initialization.""" super().__init__(parser) self.current_sibling: etree.Element | None = None self.content_indent = 0 def parse_content(self, parent: etree.Element, block: str) -> tuple[etree.Element | None, str, str]: """Get sibling admonition. Retrieve the appropriate sibling element. This can get tricky 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.tag != 'div' or sibling.get('class', '').find(self.CLASSNAME) == -1: sibling = None else: # If the last child is a list and the content is sufficiently indented # to be under it, then the content's sibling is in the list. last_child = self.lastChild(sibling) indent = 0 while last_child is not None: if ( sibling is not None and block.startswith(' ' * self.tab_length * 2) and last_child is not None and last_child.tag in ('ul', 'ol', 'dl') ): # The expectation is that we'll find an `
  • ` or `
    `. # We should get its last child as well. sibling = self.lastChild(last_child) last_child = self.lastChild(sibling) if sibling is not None else None # Context has been lost at this point, so we must adjust the # text's indentation 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: etree.Element, block: str) -> bool: if self.RE.search(block): return True else: return self.parse_content(parent, block)[0] is not None def run(self, parent: etree.Element, blocks: list[str]) -> None: 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

    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: re.Match[str]) -> tuple[str, str | None]: 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 class name as title # e.g.: `!!! note` will render # `

    Note

    ` 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)