1
1
from typing import List , Optional
2
2
3
3
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
4
7
from markdown_it .rules_inline import StateInline
5
8
from markdown_it .token import Token
6
9
@@ -46,7 +49,7 @@ def attrs_plugin(
46
49
:param span_after: The name of an inline rule after which spans may be specified.
47
50
"""
48
51
49
- def _attr_rule (state : StateInline , silent : bool ):
52
+ def _attr_inline_rule (state : StateInline , silent : bool ):
50
53
if state .pending or not state .tokens :
51
54
return False
52
55
token = state .tokens [- 1 ]
@@ -69,7 +72,29 @@ def _attr_rule(state: StateInline, silent: bool):
69
72
70
73
if spans :
71
74
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 )
73
98
74
99
75
100
def _find_opening (tokens : List [Token ], index : int ) -> Optional [int ]:
@@ -121,3 +146,83 @@ def _span_rule(state: StateInline, silent: bool):
121
146
state .pos = pos
122
147
state .posMax = maximum
123
148
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
0 commit comments