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