Skip to content

Commit 8858e5a

Browse files
authored
✨ NEW: Add attrs_block_plugin (#66)
1 parent f4f0a0e commit 8858e5a

File tree

5 files changed

+196
-6
lines changed

5 files changed

+196
-6
lines changed

docs/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,16 @@ html_string = md.render("some *Markdown*")
8989
.. autofunction:: mdit_py_plugins.admon.admon_plugin
9090
```
9191

92-
## Inline Attributes
92+
## Attributes
9393

9494
```{eval-rst}
9595
.. autofunction:: mdit_py_plugins.attrs.attrs_plugin
9696
```
9797

98+
```{eval-rst}
99+
.. autofunction:: mdit_py_plugins.attrs.attrs_block_plugin
100+
```
101+
98102
## Math
99103

100104
```{eval-rst}

mdit_py_plugins/attrs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .index import attrs_plugin # noqa: F401
1+
from .index import attrs_block_plugin, attrs_plugin # noqa: F401

mdit_py_plugins/attrs/index.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from typing import List, Optional
22

33
from markdown_it import MarkdownIt
4+
from markdown_it.common.utils import isSpace
5+
from markdown_it.rules_block import StateBlock
6+
from markdown_it.rules_core import StateCore
47
from markdown_it.rules_inline import StateInline
58
from markdown_it.token import Token
69

@@ -46,7 +49,7 @@ def attrs_plugin(
4649
:param span_after: The name of an inline rule after which spans may be specified.
4750
"""
4851

49-
def _attr_rule(state: StateInline, silent: bool):
52+
def _attr_inline_rule(state: StateInline, silent: bool):
5053
if state.pending or not state.tokens:
5154
return False
5255
token = state.tokens[-1]
@@ -69,7 +72,29 @@ def _attr_rule(state: StateInline, silent: bool):
6972

7073
if spans:
7174
md.inline.ruler.after(span_after, "span", _span_rule)
72-
md.inline.ruler.push("attr", _attr_rule)
75+
if after:
76+
md.inline.ruler.push("attr", _attr_inline_rule)
77+
78+
79+
def attrs_block_plugin(md: MarkdownIt):
80+
"""Parse block attributes.
81+
82+
Block attributes are attributes on a single line, with no other content.
83+
They attach the specified attributes to the block below them::
84+
85+
{.a #b c=1}
86+
A paragraph, that will be assigned the class ``a`` and the identifier ``b``.
87+
88+
Attributes can be stacked, with classes accumulating and lower attributes overriding higher::
89+
90+
{#a .a c=1}
91+
{#b .b c=2}
92+
A paragraph, that will be assigned the class ``a b c``, and the identifier ``b``.
93+
94+
This syntax is inspired by Djot block attributes.
95+
"""
96+
md.block.ruler.before("fence", "attr", _attr_block_rule)
97+
md.core.ruler.after("block", "attr", _attr_resolve_block_rule)
7398

7499

75100
def _find_opening(tokens: List[Token], index: int) -> Optional[int]:
@@ -121,3 +146,83 @@ def _span_rule(state: StateInline, silent: bool):
121146
state.pos = pos
122147
state.posMax = maximum
123148
return True
149+
150+
151+
def _attr_block_rule(
152+
state: StateBlock, startLine: int, endLine: int, silent: bool
153+
) -> bool:
154+
"""Find a block of attributes.
155+
156+
The block must be a single line that begins with a `{`, after three or less spaces,
157+
and end with a `}` followed by any number if spaces.
158+
"""
159+
# if it's indented more than 3 spaces, it should be a code block
160+
if state.sCount[startLine] - state.blkIndent >= 4:
161+
return False
162+
163+
pos = state.bMarks[startLine] + state.tShift[startLine]
164+
maximum = state.eMarks[startLine]
165+
166+
# if it doesn't start with a {, it's not an attribute block
167+
if state.srcCharCode[pos] != 0x7B: # /* { */
168+
return False
169+
170+
# find first non-space character from the right
171+
while maximum > pos and isSpace(state.srcCharCode[maximum - 1]):
172+
maximum -= 1
173+
# if it doesn't end with a }, it's not an attribute block
174+
if maximum <= pos:
175+
return False
176+
if state.srcCharCode[maximum - 1] != 0x7D: # /* } */
177+
return False
178+
179+
try:
180+
new_pos, attrs = parse(state.src[pos:maximum])
181+
except ParseError:
182+
return False
183+
184+
# if the block was resolved earlier than expected, it's not an attribute block
185+
# TODO this was not working in some instances, so I disabled it
186+
# if (maximum - 1) != new_pos:
187+
# return False
188+
189+
if silent:
190+
return True
191+
192+
token = state.push("attrs_block", "", 0)
193+
token.attrs = attrs # type: ignore
194+
token.map = [startLine, startLine + 1]
195+
196+
state.line = startLine + 1
197+
return True
198+
199+
200+
def _attr_resolve_block_rule(state: StateCore):
201+
"""Find attribute block then move its attributes to the next block."""
202+
i = 0
203+
len_tokens = len(state.tokens)
204+
while i < len_tokens:
205+
if state.tokens[i].type != "attrs_block":
206+
i += 1
207+
continue
208+
209+
if i + 1 < len_tokens:
210+
next_token = state.tokens[i + 1]
211+
212+
# classes are appended
213+
if "class" in state.tokens[i].attrs and "class" in next_token.attrs:
214+
state.tokens[i].attrs[
215+
"class"
216+
] = f"{state.tokens[i].attrs['class']} {next_token.attrs['class']}"
217+
218+
if next_token.type == "attrs_block":
219+
# subsequent attribute blocks take precedence, when merging
220+
for key, value in state.tokens[i].attrs.items():
221+
if key == "class" or key not in next_token.attrs:
222+
next_token.attrs[key] = value
223+
else:
224+
# attribute block takes precedence over attributes in other blocks
225+
next_token.attrs.update(state.tokens[i].attrs)
226+
227+
state.tokens.pop(i)
228+
len_tokens -= 1

tests/fixtures/attrs.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,84 @@
1+
block indented * 4 is not a block
2+
.
3+
{#a .a b=c}
4+
.
5+
<pre><code>{#a .a b=c}
6+
</code></pre>
7+
.
8+
9+
block with preceding text is not a block
10+
.
11+
{#a .a b=c} a
12+
.
13+
<p>{#a .a b=c} a</p>
14+
.
15+
16+
block no preceding
17+
.
18+
{#a .a c=1}
19+
.
20+
21+
.
22+
23+
block basic
24+
.
25+
{#a .a c=1}
26+
a
27+
.
28+
<p id="a" c="1" class="a">a</p>
29+
.
30+
31+
multiple blocks
32+
.
33+
{#a .a c=1}
34+
35+
{#b .b c=2}
36+
a
37+
.
38+
<p id="b" c="2" class="a b">a</p>
39+
.
40+
41+
block list
42+
.
43+
{#a .a c=1}
44+
- a
45+
.
46+
<ul id="a" c="1" class="a">
47+
<li>a</li>
48+
</ul>
49+
.
50+
51+
block quote
52+
.
53+
{#a .a c=1}
54+
> a
55+
.
56+
<blockquote id="a" c="1" class="a">
57+
<p>a</p>
58+
</blockquote>
59+
.
60+
61+
block fence
62+
.
63+
{#a .b c=1}
64+
```python
65+
a = 1
66+
```
67+
.
68+
<pre><code id="a" c="1" class="b language-python">a = 1
69+
</code></pre>
70+
.
71+
72+
block after paragraph
73+
.
74+
a
75+
{#a .a c=1}
76+
.
77+
<p>a
78+
{#a .a c=1}</p>
79+
.
80+
81+
182
simple reference link
283
.
384
[text *emphasis*](a){#id .a}

tests/test_attrs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from markdown_it.utils import read_fixture_file
55
import pytest
66

7-
from mdit_py_plugins.attrs import attrs_plugin
7+
from mdit_py_plugins.attrs import attrs_block_plugin, attrs_plugin
88

99
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
1010

@@ -13,7 +13,7 @@
1313
"line,title,input,expected", read_fixture_file(FIXTURE_PATH / "attrs.md")
1414
)
1515
def test_attrs(line, title, input, expected):
16-
md = MarkdownIt("commonmark").use(attrs_plugin, spans=True)
16+
md = MarkdownIt("commonmark").use(attrs_plugin, spans=True).use(attrs_block_plugin)
1717
md.options["xhtmlOut"] = False
1818
text = md.render(input)
1919
print(text)

0 commit comments

Comments
 (0)