Skip to content

Commit ec51e60

Browse files
authored
✨ NEW: Port admon plugin (#53)
Add plugin for parsing python-markdown style admonitions
1 parent 3f90276 commit ec51e60

File tree

7 files changed

+495
-0
lines changed

7 files changed

+495
-0
lines changed

docs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ html_string = md.render("some *Markdown*")
8585
.. autofunction:: mdit_py_plugins.container.container_plugin
8686
```
8787

88+
```{eval-rst}
89+
.. autofunction:: mdit_py_plugins.admon.admon_plugin
90+
```
91+
8892
## Inline Attributes
8993

9094
```{eval-rst}

mdit_py_plugins/admon/LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
2+
Copyright (c) 2018 jebbs
3+
Copyright (c) 2021- commenthol
4+
5+
Permission is hereby granted, free of charge, to any person
6+
obtaining a copy of this software and associated documentation
7+
files (the "Software"), to deal in the Software without
8+
restriction, including without limitation the rights to use,
9+
copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following
12+
conditions:
13+
14+
The above copyright notice and this permission notice shall be
15+
included in all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24+
OTHER DEALINGS IN THE SOFTWARE.

mdit_py_plugins/admon/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .index import admon_plugin # noqa: F401

mdit_py_plugins/admon/index.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Process admonitions and pass to cb.
2+
3+
import math
4+
from typing import Callable, Optional, Tuple
5+
6+
from markdown_it import MarkdownIt
7+
from markdown_it.rules_block import StateBlock
8+
9+
10+
def get_tag(params: str) -> Tuple[str, str]:
11+
if not params.strip():
12+
return "", ""
13+
14+
tag, *_title = params.strip().split(" ")
15+
joined = " ".join(_title)
16+
17+
title = ""
18+
if not joined:
19+
title = tag.title()
20+
elif joined != '""':
21+
title = joined
22+
return tag.lower(), title
23+
24+
25+
def validate(params: str) -> bool:
26+
tag = params.strip().split(" ", 1)[-1] or ""
27+
return bool(tag)
28+
29+
30+
MIN_MARKERS = 3
31+
MARKER_STR = "!"
32+
MARKER_CHAR = ord(MARKER_STR)
33+
MARKER_LEN = len(MARKER_STR)
34+
35+
36+
def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
37+
start = state.bMarks[startLine] + state.tShift[startLine]
38+
maximum = state.eMarks[startLine]
39+
40+
# Check out the first character quickly, which should filter out most of non-containers
41+
if MARKER_CHAR != ord(state.src[start]):
42+
return False
43+
44+
# 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:
51+
return False
52+
marker_pos = pos - ((pos - start) % MARKER_LEN)
53+
params = state.src[marker_pos:maximum]
54+
markup = state.src[start:marker_pos]
55+
56+
if not validate(params):
57+
return False
58+
59+
# Since start is found, we can report success here in validation mode
60+
if silent:
61+
return True
62+
63+
old_parent = state.parentType
64+
old_line_max = state.lineMax
65+
old_indent = state.blkIndent
66+
67+
blk_start = pos
68+
while blk_start < maximum and state.src[blk_start] == " ":
69+
blk_start += 1
70+
71+
state.parentType = "admonition"
72+
state.blkIndent += blk_start - start
73+
74+
was_empty = False
75+
76+
# Search for the end of the block
77+
next_line = startLine
78+
while True:
79+
next_line += 1
80+
if next_line >= endLine:
81+
# unclosed block should be autoclosed by end of document.
82+
# also block seems to be autoclosed by end of parent
83+
break
84+
pos = state.bMarks[next_line] + state.tShift[next_line]
85+
maximum = state.eMarks[next_line]
86+
is_empty = state.sCount[next_line] < state.blkIndent
87+
88+
# two consecutive empty lines autoclose the block
89+
if is_empty and was_empty:
90+
break
91+
was_empty = is_empty
92+
93+
if pos < maximum and state.sCount[next_line] < state.blkIndent:
94+
# non-empty line with negative indent should stop the block:
95+
# - !!!
96+
# test
97+
break
98+
99+
# this will prevent lazy continuations from ever going past our end marker
100+
state.lineMax = next_line
101+
102+
tag, title = get_tag(params)
103+
104+
token = state.push("admonition_open", "div", 1)
105+
token.markup = markup
106+
token.block = True
107+
token.attrs = {"class": f"admonition {tag}"}
108+
token.meta = {"tag": tag}
109+
token.content = title
110+
token.info = params
111+
token.map = [startLine, next_line]
112+
113+
if title:
114+
title_markup = f"{markup} {tag}"
115+
token = state.push("admonition_title_open", "p", 1)
116+
token.markup = title_markup
117+
token.attrs = {"class": "admonition-title"}
118+
token.map = [startLine, startLine + 1]
119+
120+
token = state.push("inline", "", 0)
121+
token.content = title
122+
token.map = [startLine, startLine + 1]
123+
token.children = []
124+
125+
token = state.push("admonition_title_close", "p", -1)
126+
token.markup = title_markup
127+
128+
state.md.block.tokenize(state, startLine + 1, next_line)
129+
130+
token = state.push("admonition_close", "div", -1)
131+
token.markup = state.src[start:pos]
132+
token.block = True
133+
134+
state.parentType = old_parent
135+
state.lineMax = old_line_max
136+
state.blkIndent = old_indent
137+
state.line = next_line
138+
139+
return True
140+
141+
142+
def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None:
143+
"""Plugin to use
144+
`python-markdown style admonitions
145+
<https://python-markdown.github.io/extensions/admonition>`_.
146+
147+
.. code-block:: md
148+
149+
!!! note
150+
*content*
151+
152+
Note, this is ported from
153+
`markdown-it-admon
154+
<https://github.com/commenthol/markdown-it-admon>`_.
155+
"""
156+
157+
def renderDefault(self, tokens, idx, _options, env):
158+
return self.renderToken(tokens, idx, _options, env)
159+
160+
render = render or renderDefault
161+
162+
md.add_render_rule("admonition_open", render)
163+
md.add_render_rule("admonition_close", render)
164+
md.add_render_rule("admonition_title_open", render)
165+
md.add_render_rule("admonition_title_close", render)
166+
167+
md.block.ruler.before(
168+
"fence",
169+
"admonition",
170+
admonition,
171+
{"alt": ["paragraph", "reference", "blockquote", "list"]},
172+
)

mdit_py_plugins/admon/port.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- package: markdown-it-admon
2+
commit: 9820ba89415c464a3cc18a780f222a0ceb3e18bd
3+
date: Jul 3, 2021
4+
version: 1.0.0

0 commit comments

Comments
 (0)