123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- # Process admonitions and pass to cb.
- from __future__ import annotations
- from contextlib import suppress
- import re
- from typing import TYPE_CHECKING, Callable, Sequence
- from markdown_it import MarkdownIt
- from markdown_it.rules_block import StateBlock
- from mdit_py_plugins.utils import is_code_block
- if TYPE_CHECKING:
- from markdown_it.renderer import RendererProtocol
- from markdown_it.token import Token
- from markdown_it.utils import EnvType, OptionsDict
- def _get_multiple_tags(params: str) -> tuple[list[str], str]:
- """Check for multiple tags when the title is double quoted."""
- re_tags = re.compile(r'^\s*(?P<tokens>[^"]+)\s+"(?P<title>.*)"\S*$')
- match = re_tags.match(params)
- if match:
- tags = match["tokens"].strip().split(" ")
- return [tag.lower() for tag in tags], match["title"]
- raise ValueError("No match found for parameters")
- def _get_tag(_params: str) -> tuple[list[str], str]:
- """Separate the tag name from the admonition title."""
- params = _params.strip()
- if not params:
- return [""], ""
- with suppress(ValueError):
- return _get_multiple_tags(params)
- tag, *_title = params.split(" ")
- joined = " ".join(_title)
- title = ""
- if not joined:
- title = tag.title()
- elif joined != '""': # Specifically check for no title
- title = joined
- return [tag.lower()], title
- def _validate(params: str) -> bool:
- """Validate the presence of the tag name after the marker."""
- tag = params.strip().split(" ", 1)[-1] or ""
- return bool(tag)
- MARKER_LEN = 3 # Regardless of extra characters, block indent stays the same
- MARKERS = ("!!!", "???", "???+")
- MARKER_CHARS = {_m[0] for _m in MARKERS}
- MAX_MARKER_LEN = max(len(_m) for _m in MARKERS)
- def _extra_classes(markup: str) -> list[str]:
- """Return the list of additional classes based on the markup."""
- if markup.startswith("?"):
- if markup.endswith("+"):
- return ["is-collapsible collapsible-open"]
- return ["is-collapsible collapsible-closed"]
- return []
- def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
- if is_code_block(state, startLine):
- return False
- start = state.bMarks[startLine] + state.tShift[startLine]
- maximum = state.eMarks[startLine]
- # Check out the first character quickly, which should filter out most of non-containers
- if state.src[start] not in MARKER_CHARS:
- return False
- # Check out the rest of the marker string
- marker = ""
- marker_len = MAX_MARKER_LEN
- while marker_len > 0:
- marker_pos = start + marker_len
- markup = state.src[start:marker_pos]
- if markup in MARKERS:
- marker = markup
- break
- marker_len -= 1
- else:
- return False
- params = state.src[marker_pos:maximum]
- if not _validate(params):
- return False
- # Since start is found, we can report success here in validation mode
- if silent:
- return True
- old_parent = state.parentType
- old_line_max = state.lineMax
- old_indent = state.blkIndent
- blk_start = marker_pos
- while blk_start < maximum and state.src[blk_start] == " ":
- blk_start += 1
- state.parentType = "admonition"
- # Correct block indentation when extra marker characters are present
- marker_alignment_correction = MARKER_LEN - len(marker)
- state.blkIndent += blk_start - start + marker_alignment_correction
- was_empty = False
- # Search for the end of the block
- next_line = startLine
- while True:
- next_line += 1
- if next_line >= endLine:
- # unclosed block should be autoclosed by end of document.
- # also block seems to be autoclosed by end of parent
- break
- pos = state.bMarks[next_line] + state.tShift[next_line]
- maximum = state.eMarks[next_line]
- is_empty = state.sCount[next_line] < state.blkIndent
- # two consecutive empty lines autoclose the block
- if is_empty and was_empty:
- break
- was_empty = is_empty
- if pos < maximum and state.sCount[next_line] < state.blkIndent:
- # non-empty line with negative indent should stop the block:
- # - !!!
- # test
- break
- # this will prevent lazy continuations from ever going past our end marker
- state.lineMax = next_line
- tags, title = _get_tag(params)
- tag = tags[0]
- token = state.push("admonition_open", "div", 1)
- token.markup = markup
- token.block = True
- token.attrs = {"class": " ".join(["admonition", *tags, *_extra_classes(markup)])}
- token.meta = {"tag": tag}
- token.content = title
- token.info = params
- token.map = [startLine, next_line]
- if title:
- title_markup = f"{markup} {tag}"
- token = state.push("admonition_title_open", "p", 1)
- token.markup = title_markup
- token.attrs = {"class": "admonition-title"}
- token.map = [startLine, startLine + 1]
- token = state.push("inline", "", 0)
- token.content = title
- token.map = [startLine, startLine + 1]
- token.children = []
- token = state.push("admonition_title_close", "p", -1)
- state.md.block.tokenize(state, startLine + 1, next_line)
- token = state.push("admonition_close", "div", -1)
- token.markup = markup
- token.block = True
- state.parentType = old_parent
- state.lineMax = old_line_max
- state.blkIndent = old_indent
- state.line = next_line
- return True
- def admon_plugin(md: MarkdownIt, render: None | Callable[..., str] = None) -> None:
- """Plugin to use
- `python-markdown style admonitions
- <https://python-markdown.github.io/extensions/admonition>`_.
- .. code-block:: md
- !!! note
- *content*
- `And mkdocs-style collapsible blocks
- <https://squidfunk.github.io/mkdocs-material/reference/admonitions/#collapsible-blocks>`_.
- .. code-block:: md
- ???+ note
- *content*
- Note, this is ported from
- `markdown-it-admon
- <https://github.com/commenthol/markdown-it-admon>`_.
- """
- def renderDefault(
- self: RendererProtocol,
- tokens: Sequence[Token],
- idx: int,
- _options: OptionsDict,
- env: EnvType,
- ) -> str:
- return self.renderToken(tokens, idx, _options, env) # type: ignore[attr-defined,no-any-return]
- render = render or renderDefault
- md.add_render_rule("admonition_open", render)
- md.add_render_rule("admonition_close", render)
- md.add_render_rule("admonition_title_open", render)
- md.add_render_rule("admonition_title_close", render)
- md.block.ruler.before(
- "fence",
- "admonition",
- admonition,
- {"alt": ["paragraph", "reference", "blockquote", "list"]},
- )
|