Skip to content

Commit

Permalink
Merge pull request #16 from KyleKing/refactor-separate-markers
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleKing authored Jan 14, 2024
2 parents cb252b1 + 00a6b70 commit 3c5ea23
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 549 deletions.
234 changes: 13 additions & 221 deletions mdformat_mkdocs/mdit_py_plugins/admon/index.py
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)
12 changes: 5 additions & 7 deletions mdformat_mkdocs/mdit_py_plugins/admon_helpers/__init__.py
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,
)
Loading

0 comments on commit 3c5ea23

Please sign in to comment.