Skip to content

Commit 9cb9ff9

Browse files
👌 Improve admon plugin (add ??? support) (#58)
Extend the `admon` plugin for the `mkdocs` `?`-based collapsible syntax by displaying as a regular admonition Co-authored-by: Chris Sewell <[email protected]>
1 parent 061a544 commit 9cb9ff9

File tree

6 files changed

+519
-24
lines changed

6 files changed

+519
-24
lines changed

‎mdit_py_plugins/admon/index.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Process admonitions and pass to cb.
22

3-
import math
4-
from typing import Callable, Optional, Tuple
3+
from typing import Callable, List, Optional, Tuple
54

65
from markdown_it import MarkdownIt
76
from markdown_it.rules_block import StateBlock
87

98

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

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

2424

25-
def validate(params: str) -> bool:
25+
def _validate(params: str) -> bool:
26+
"""Validate the presence of the tag name after the marker."""
2627
tag = params.strip().split(" ", 1)[-1] or ""
2728
return bool(tag)
2829

2930

30-
MIN_MARKERS = 3
31-
MARKER_STR = "!"
32-
MARKER_CHAR = ord(MARKER_STR)
33-
MARKER_LEN = len(MARKER_STR)
31+
MARKER_LEN = 3 # Regardless of extra characters, block indent stays the same
32+
MARKERS = ("!!!", "???", "???+")
33+
MARKER_CHARS = {_m[0] for _m in MARKERS}
34+
MAX_MARKER_LEN = max(len(_m) for _m in MARKERS)
35+
36+
37+
def _extra_classes(markup: str) -> List[str]:
38+
"""Return the list of additional classes based on the markup."""
39+
if markup.startswith("?"):
40+
if markup.endswith("+"):
41+
return ["is-collapsible collapsible-open"]
42+
return ["is-collapsible collapsible-closed"]
43+
return []
3444

3545

3646
def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
3747
start = state.bMarks[startLine] + state.tShift[startLine]
3848
maximum = state.eMarks[startLine]
3949

4050
# Check out the first character quickly, which should filter out most of non-containers
41-
if ord(state.src[start]) != MARKER_CHAR:
51+
if state.src[start] not in MARKER_CHARS:
4252
return False
4353

4454
# Check out the rest of the marker string
45-
pos = start + 1
46-
while pos <= maximum and MARKER_STR[(pos - start) % MARKER_LEN] == state.src[pos]:
47-
pos += 1
48-
49-
marker_count = math.floor((pos - start) / MARKER_LEN)
50-
if marker_count < MIN_MARKERS:
55+
marker = ""
56+
marker_len = MAX_MARKER_LEN
57+
while marker_len > 0:
58+
marker_pos = start + marker_len
59+
markup = state.src[start:marker_pos]
60+
if markup in MARKERS:
61+
marker = markup
62+
break
63+
marker_len -= 1
64+
else:
5165
return False
52-
marker_pos = pos - ((pos - start) % MARKER_LEN)
66+
5367
params = state.src[marker_pos:maximum]
54-
markup = state.src[start:marker_pos]
5568

56-
if not validate(params):
69+
if not _validate(params):
5770
return False
5871

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

67-
blk_start = pos
80+
blk_start = marker_pos
6881
while blk_start < maximum and state.src[blk_start] == " ":
6982
blk_start += 1
7083

7184
state.parentType = "admonition"
72-
state.blkIndent += blk_start - start
85+
# Correct block indentation when extra marker characters are present
86+
marker_alignment_correction = MARKER_LEN - len(marker)
87+
state.blkIndent += blk_start - start + marker_alignment_correction
7388

7489
was_empty = False
7590

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

102-
tag, title = get_tag(params)
117+
tag, title = _get_tag(params)
103118

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

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

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

130144
token = state.push("admonition_close", "div", -1)
131-
token.markup = state.src[start:pos]
145+
token.markup = markup
132146
token.block = True
133147

134148
state.parentType = old_parent
@@ -149,6 +163,14 @@ def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None:
149163
!!! note
150164
*content*
151165
166+
`And mkdocs-style collapsible blocks
167+
<https://squidfunk.github.io/mkdocs-material/reference/admonitions/#collapsible-blocks>`_.
168+
169+
.. code-block:: md
170+
171+
???+ note
172+
*content*
173+
152174
Note, this is ported from
153175
`markdown-it-admon
154176
<https://github.com/commenthol/markdown-it-admon>`_.

‎tests/fixtures/admon.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,28 @@ Does not render
267267
<p>!!!
268268
content</p>
269269
.
270+
271+
272+
273+
MKdocs Closed Collapsible Sections
274+
.
275+
??? note
276+
content
277+
.
278+
<div class="admonition note is-collapsible collapsible-closed">
279+
<p class="admonition-title">Note</p>
280+
<p>content</p>
281+
</div>
282+
.
283+
284+
285+
MKdocs Open Collapsible Sections
286+
.
287+
???+ note
288+
content
289+
.
290+
<div class="admonition note is-collapsible collapsible-open">
291+
<p class="admonition-title">Note</p>
292+
<p>content</p>
293+
</div>
294+
.

‎tests/test_admon.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pathlib import Path
2+
from textwrap import dedent
23

34
from markdown_it import MarkdownIt
45
from markdown_it.utils import read_fixture_file
@@ -19,3 +20,15 @@ def test_all(line, title, input, expected):
1920
text = md.render(input)
2021
print(text)
2122
assert text.rstrip() == expected.rstrip()
23+
24+
25+
@pytest.mark.parametrize("text_idx", (0, 1, 2))
26+
def test_plugin_parse(data_regression, text_idx):
27+
texts = [
28+
"!!! note\n content 1",
29+
"??? note\n content 2",
30+
"???+ note\n content 3",
31+
]
32+
md = MarkdownIt().use(admon_plugin)
33+
tokens = md.parse(dedent(texts[text_idx]))
34+
data_regression.check([t.as_dict() for t in tokens])
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
- attrs:
2+
- - class
3+
- admonition note
4+
block: true
5+
children: null
6+
content: Note
7+
hidden: false
8+
info: ' note'
9+
level: 0
10+
map:
11+
- 0
12+
- 2
13+
markup: '!!!'
14+
meta:
15+
tag: note
16+
nesting: 1
17+
tag: div
18+
type: admonition_open
19+
- attrs:
20+
- - class
21+
- admonition-title
22+
block: true
23+
children: null
24+
content: ''
25+
hidden: false
26+
info: ''
27+
level: 1
28+
map:
29+
- 0
30+
- 1
31+
markup: '!!! note'
32+
meta: {}
33+
nesting: 1
34+
tag: p
35+
type: admonition_title_open
36+
- attrs: null
37+
block: true
38+
children:
39+
- attrs: null
40+
block: false
41+
children: null
42+
content: Note
43+
hidden: false
44+
info: ''
45+
level: 0
46+
map: null
47+
markup: ''
48+
meta: {}
49+
nesting: 0
50+
tag: ''
51+
type: text
52+
content: Note
53+
hidden: false
54+
info: ''
55+
level: 2
56+
map:
57+
- 0
58+
- 1
59+
markup: ''
60+
meta: {}
61+
nesting: 0
62+
tag: ''
63+
type: inline
64+
- attrs: null
65+
block: true
66+
children: null
67+
content: ''
68+
hidden: false
69+
info: ''
70+
level: 1
71+
map: null
72+
markup: ''
73+
meta: {}
74+
nesting: -1
75+
tag: p
76+
type: admonition_title_close
77+
- attrs: null
78+
block: true
79+
children: null
80+
content: ''
81+
hidden: false
82+
info: ''
83+
level: 1
84+
map:
85+
- 1
86+
- 2
87+
markup: ''
88+
meta: {}
89+
nesting: 1
90+
tag: p
91+
type: paragraph_open
92+
- attrs: null
93+
block: true
94+
children:
95+
- attrs: null
96+
block: false
97+
children: null
98+
content: content 1
99+
hidden: false
100+
info: ''
101+
level: 0
102+
map: null
103+
markup: ''
104+
meta: {}
105+
nesting: 0
106+
tag: ''
107+
type: text
108+
content: content 1
109+
hidden: false
110+
info: ''
111+
level: 2
112+
map:
113+
- 1
114+
- 2
115+
markup: ''
116+
meta: {}
117+
nesting: 0
118+
tag: ''
119+
type: inline
120+
- attrs: null
121+
block: true
122+
children: null
123+
content: ''
124+
hidden: false
125+
info: ''
126+
level: 1
127+
map: null
128+
markup: ''
129+
meta: {}
130+
nesting: -1
131+
tag: p
132+
type: paragraph_close
133+
- attrs: null
134+
block: true
135+
children: null
136+
content: ''
137+
hidden: false
138+
info: ''
139+
level: 0
140+
map: null
141+
markup: '!!!'
142+
meta: {}
143+
nesting: -1
144+
tag: div
145+
type: admonition_close

0 commit comments

Comments
 (0)