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

👌 Improve admon plugin (add ??? support) #58

Merged
merged 19 commits into from
Apr 27, 2023
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
70 changes: 46 additions & 24 deletions mdit_py_plugins/admon/index.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Process admonitions and pass to cb.

import math
from typing import Callable, Optional, Tuple
from typing import Callable, List, Optional, Tuple

from markdown_it import MarkdownIt
from markdown_it.rules_block import StateBlock


def get_tag(params: str) -> Tuple[str, str]:
def _get_tag(params: str) -> Tuple[str, str]:
"""Separate the tag name from the admonition title."""
if not params.strip():
return "", ""

Expand All @@ -22,38 +22,51 @@ def get_tag(params: str) -> Tuple[str, str]:
return tag.lower(), title


def validate(params: str) -> bool:
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)


MIN_MARKERS = 3
MARKER_STR = "!"
MARKER_CHAR = ord(MARKER_STR)
MARKER_LEN = len(MARKER_STR)
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:
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 ord(state.src[start]) != MARKER_CHAR:
if state.src[start] not in MARKER_CHARS:
return False

# Check out the rest of the marker string
pos = start + 1
while pos <= maximum and MARKER_STR[(pos - start) % MARKER_LEN] == state.src[pos]:
pos += 1

marker_count = math.floor((pos - start) / MARKER_LEN)
if marker_count < MIN_MARKERS:
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
marker_pos = pos - ((pos - start) % MARKER_LEN)

params = state.src[marker_pos:maximum]
markup = state.src[start:marker_pos]

if not validate(params):
if not _validate(params):
return False

# Since start is found, we can report success here in validation mode
Expand All @@ -64,12 +77,14 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) ->
old_line_max = state.lineMax
old_indent = state.blkIndent

blk_start = pos
blk_start = marker_pos
while blk_start < maximum and state.src[blk_start] == " ":
blk_start += 1

state.parentType = "admonition"
state.blkIndent += blk_start - start
# 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

Expand Down Expand Up @@ -99,12 +114,12 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) ->
# this will prevent lazy continuations from ever going past our end marker
state.lineMax = next_line

tag, title = get_tag(params)
tag, title = _get_tag(params)

token = state.push("admonition_open", "div", 1)
token.markup = markup
token.block = True
token.attrs = {"class": f"admonition {tag}"}
token.attrs = {"class": " ".join(["admonition", tag, *_extra_classes(markup)])}
token.meta = {"tag": tag}
token.content = title
token.info = params
Expand All @@ -123,12 +138,11 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) ->
token.children = []

token = state.push("admonition_title_close", "p", -1)
token.markup = title_markup

state.md.block.tokenize(state, startLine + 1, next_line)

token = state.push("admonition_close", "div", -1)
token.markup = state.src[start:pos]
token.markup = markup
KyleKing marked this conversation as resolved.
Show resolved Hide resolved
token.block = True

state.parentType = old_parent
Expand All @@ -149,6 +163,14 @@ def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None:
!!! 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>`_.
Expand Down
25 changes: 25 additions & 0 deletions tests/fixtures/admon.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,28 @@ Does not render
<p>!!!
content</p>
.



MKdocs Closed Collapsible Sections
.
??? note
content
.
<div class="admonition note is-collapsible collapsible-closed">
<p class="admonition-title">Note</p>
<p>content</p>
</div>
.


MKdocs Open Collapsible Sections
.
???+ note
content
.
<div class="admonition note is-collapsible collapsible-open">
<p class="admonition-title">Note</p>
<p>content</p>
</div>
.
13 changes: 13 additions & 0 deletions tests/test_admon.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pathlib import Path
from textwrap import dedent

from markdown_it import MarkdownIt
from markdown_it.utils import read_fixture_file
Expand All @@ -19,3 +20,15 @@ def test_all(line, title, input, expected):
text = md.render(input)
print(text)
assert text.rstrip() == expected.rstrip()


@pytest.mark.parametrize("text_idx", (0, 1, 2))
def test_plugin_parse(data_regression, text_idx):
texts = [
"!!! note\n content 1",
"??? note\n content 2",
"???+ note\n content 3",
]
md = MarkdownIt().use(admon_plugin)
tokens = md.parse(dedent(texts[text_idx]))
data_regression.check([t.as_dict() for t in tokens])
145 changes: 145 additions & 0 deletions tests/test_admon/test_plugin_parse_0_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
- attrs:
- - class
- admonition note
block: true
children: null
content: Note
hidden: false
info: ' note'
level: 0
map:
- 0
- 2
markup: '!!!'
meta:
tag: note
nesting: 1
tag: div
type: admonition_open
- attrs:
- - class
- admonition-title
block: true
children: null
content: ''
hidden: false
info: ''
level: 1
map:
- 0
- 1
markup: '!!! note'
meta: {}
nesting: 1
tag: p
type: admonition_title_open
- attrs: null
block: true
children:
- attrs: null
block: false
children: null
content: Note
hidden: false
info: ''
level: 0
map: null
markup: ''
meta: {}
nesting: 0
tag: ''
type: text
content: Note
hidden: false
info: ''
level: 2
map:
- 0
- 1
markup: ''
meta: {}
nesting: 0
tag: ''
type: inline
- attrs: null
block: true
children: null
content: ''
hidden: false
info: ''
level: 1
map: null
markup: ''
meta: {}
nesting: -1
tag: p
type: admonition_title_close
- attrs: null
block: true
children: null
content: ''
hidden: false
info: ''
level: 1
map:
- 1
- 2
markup: ''
meta: {}
nesting: 1
tag: p
type: paragraph_open
- attrs: null
block: true
children:
- attrs: null
block: false
children: null
content: content 1
hidden: false
info: ''
level: 0
map: null
markup: ''
meta: {}
nesting: 0
tag: ''
type: text
content: content 1
hidden: false
info: ''
level: 2
map:
- 1
- 2
markup: ''
meta: {}
nesting: 0
tag: ''
type: inline
- attrs: null
block: true
children: null
content: ''
hidden: false
info: ''
level: 1
map: null
markup: ''
meta: {}
nesting: -1
tag: p
type: paragraph_close
- attrs: null
block: true
children: null
content: ''
hidden: false
info: ''
level: 0
map: null
markup: '!!!'
meta: {}
nesting: -1
tag: div
type: admonition_close
Loading