Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement separate python markdown and material mkdocs logic #16

Merged
merged 7 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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