generated from executablebooks/mdformat-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from KyleKing/refactor-separate-markers
- Loading branch information
Showing
11 changed files
with
559 additions
and
549 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,230 +1,22 @@ | ||
# Process admonitions and pass to cb. | ||
|
||
from __future__ import annotations | ||
|
||
from contextlib import suppress | ||
import re | ||
from typing import TYPE_CHECKING, Callable, List, Sequence, Tuple | ||
|
||
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) | ||
|
||
from ..admon_helpers import ( | ||
Admonition, | ||
admon_plugin_factory, | ||
format_python_markdown_admon_markup, | ||
parse_possible_admon_factory, | ||
) | ||
|
||
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 | ||
def admonition_logic( | ||
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] | ||
|
||
# Exit quickly on a non-match for first char | ||
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: | ||
parse_possible_admon = parse_possible_admon_factory(markers={"!!!"}) | ||
result = parse_possible_admon(state, startLine, endLine, silent) | ||
if isinstance(result, Admonition): | ||
format_python_markdown_admon_markup(state, startLine, admonition=result) | ||
return True | ||
return result | ||
|
||
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 | ||
|
||
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"]}, | ||
) | ||
admon_plugin = admon_plugin_factory("admonition", admonition_logic) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,8 @@ | ||
from .helpers import ( # noqa: F401 | ||
admon_plugin_wrapper, | ||
admonition_logic, | ||
default_render, | ||
format_admon_markup, | ||
from ._helpers import ( # noqa: F401 | ||
Admonition, | ||
admon_plugin_factory, | ||
format_python_markdown_admon_markup, | ||
new_token, | ||
parse_possible_admon, | ||
parse_possible_admon_factory, | ||
parse_tag_and_title, | ||
validate_admon_meta, | ||
) |
Oops, something went wrong.