From 011e09896db4302d92b65a82a64cc644992dc5e7 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Wed, 10 Jan 2024 05:13:03 -0500 Subject: [PATCH] feat(#14): copy latest mdit-py-plugins code --- mdformat_admon/_local/__init__.py | 0 .../_local/mdit_py_plugins/__init__.py | 0 .../_local/mdit_py_plugins/admon/__init__.py | 3 + .../_local/mdit_py_plugins/admon/index.py | 215 ++++++++++++++++++ mdformat_admon/plugin.py | 4 +- 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 mdformat_admon/_local/__init__.py create mode 100644 mdformat_admon/_local/mdit_py_plugins/__init__.py create mode 100644 mdformat_admon/_local/mdit_py_plugins/admon/__init__.py create mode 100644 mdformat_admon/_local/mdit_py_plugins/admon/index.py diff --git a/mdformat_admon/_local/__init__.py b/mdformat_admon/_local/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mdformat_admon/_local/mdit_py_plugins/__init__.py b/mdformat_admon/_local/mdit_py_plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mdformat_admon/_local/mdit_py_plugins/admon/__init__.py b/mdformat_admon/_local/mdit_py_plugins/admon/__init__.py new file mode 100644 index 0000000..27e968d --- /dev/null +++ b/mdformat_admon/_local/mdit_py_plugins/admon/__init__.py @@ -0,0 +1,3 @@ +from .index import admon_plugin + +__all__ = ("admon_plugin",) diff --git a/mdformat_admon/_local/mdit_py_plugins/admon/index.py b/mdformat_admon/_local/mdit_py_plugins/admon/index.py new file mode 100644 index 0000000..aa0e383 --- /dev/null +++ b/mdformat_admon/_local/mdit_py_plugins/admon/index.py @@ -0,0 +1,215 @@ +# FYI: copied from `mdit-py-plugins` +# https://github.com/executablebooks/mdit-py-plugins/blob/ba0c31e762d4961b6fde1b27541be92084af877b/mdit_py_plugins/admon/index.py + +# Process admonitions and pass to cb. +from __future__ import annotations + +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_tag(params: str) -> tuple[str, str]: + """Separate the tag name from the admonition title.""" + if not params.strip(): + return "", "" + + tag, *_title = params.strip().split(" ") + joined = " ".join(_title) + + title = "" + if not joined: + title = tag.title() + elif joined != '""': + 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( # noqa: C901 + 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 + + tag, title = _get_tag(params) + + token = state.push("admonition_open", "div", 1) + token.markup = markup + token.block = True + token.attrs = {"class": " ".join(["admonition", tag, *_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 + `_. + + .. code-block:: md + + !!! note + *content* + + `And mkdocs-style collapsible blocks + `_. + + .. code-block:: md + + ???+ note + *content* + + Note, this is ported from + `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 + + 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"]}, + ) diff --git a/mdformat_admon/plugin.py b/mdformat_admon/plugin.py index 43f22b3..57e958f 100644 --- a/mdformat_admon/plugin.py +++ b/mdformat_admon/plugin.py @@ -4,7 +4,9 @@ from markdown_it import MarkdownIt from mdformat.renderer import RenderContext, RenderTreeNode from mdformat.renderer.typing import Render -from mdit_py_plugins.admon import admon_plugin + +# TODO: continue to ping mdit_py_plugins owner to release these changes +from ._local.mdit_py_plugins.admon import admon_plugin def update_mdit(mdit: MarkdownIt) -> None: