Spaces:
Running
Running
# 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 `<li>` or `<dt>`. | |
# 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 <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: 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 | |
# `<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) | |