1
- from typing import List , Optional , Sequence
1
+ from __future__ import annotations
2
+
3
+ from functools import partial
4
+ from typing import Any , Sequence
2
5
3
6
from markdown_it import MarkdownIt
4
7
from markdown_it .rules_block import StateBlock
@@ -17,6 +20,7 @@ def attrs_plugin(
17
20
after : Sequence [str ] = ("image" , "code_inline" , "link_close" , "span_close" ),
18
21
spans : bool = False ,
19
22
span_after : str = "link" ,
23
+ allowed : Sequence [str ] | None = None ,
20
24
) -> None :
21
25
"""Parse inline attributes that immediately follow certain inline elements::
22
26
@@ -48,36 +52,25 @@ def attrs_plugin(
48
52
:param spans: If True, also parse attributes after spans of text, encapsulated by `[]`.
49
53
Note Markdown link references take precedence over this syntax.
50
54
:param span_after: The name of an inline rule after which spans may be specified.
55
+ :param allowed: A list of allowed attribute names.
56
+ If not ``None``, any attributes not in this list will be removed
57
+ and placed in the token's meta under the key "insecure_attrs".
51
58
"""
52
59
53
- def _attr_inline_rule (state : StateInline , silent : bool ) -> bool :
54
- if state .pending or not state .tokens :
55
- return False
56
- token = state .tokens [- 1 ]
57
- if token .type not in after :
58
- return False
59
- try :
60
- new_pos , attrs = parse (state .src [state .pos :])
61
- except ParseError :
62
- return False
63
- token_index = _find_opening (state .tokens , len (state .tokens ) - 1 )
64
- if token_index is None :
65
- return False
66
- state .pos += new_pos + 1
67
- if not silent :
68
- attr_token = state .tokens [token_index ]
69
- if "class" in attrs and "class" in token .attrs :
70
- attrs ["class" ] = f"{ attr_token .attrs ['class' ]} { attrs ['class' ]} "
71
- attr_token .attrs .update (attrs )
72
- return True
73
-
74
60
if spans :
75
61
md .inline .ruler .after (span_after , "span" , _span_rule )
76
62
if after :
77
- md .inline .ruler .push ("attr" , _attr_inline_rule )
63
+ md .inline .ruler .push (
64
+ "attr" ,
65
+ partial (
66
+ _attr_inline_rule ,
67
+ after = after ,
68
+ allowed = None if allowed is None else set (allowed ),
69
+ ),
70
+ )
78
71
79
72
80
- def attrs_block_plugin (md : MarkdownIt ) -> None :
73
+ def attrs_block_plugin (md : MarkdownIt , * , allowed : Sequence [ str ] | None = None ) -> None :
81
74
"""Parse block attributes.
82
75
83
76
Block attributes are attributes on a single line, with no other content.
@@ -93,12 +86,22 @@ def attrs_block_plugin(md: MarkdownIt) -> None:
93
86
A paragraph, that will be assigned the class ``a b c``, and the identifier ``b``.
94
87
95
88
This syntax is inspired by Djot block attributes.
89
+
90
+ :param allowed: A list of allowed attribute names.
91
+ If not ``None``, any attributes not in this list will be removed
92
+ and placed in the token's meta under the key "insecure_attrs".
96
93
"""
97
94
md .block .ruler .before ("fence" , "attr" , _attr_block_rule )
98
- md .core .ruler .after ("block" , "attr" , _attr_resolve_block_rule )
95
+ md .core .ruler .after (
96
+ "block" ,
97
+ "attr" ,
98
+ partial (
99
+ _attr_resolve_block_rule , allowed = None if allowed is None else set (allowed )
100
+ ),
101
+ )
99
102
100
103
101
- def _find_opening (tokens : List [Token ], index : int ) -> Optional [ int ] :
104
+ def _find_opening (tokens : Sequence [Token ], index : int ) -> int | None :
102
105
"""Find the opening token index, if the token is closing."""
103
106
if tokens [index ].nesting != - 1 :
104
107
return index
@@ -149,6 +152,34 @@ def _span_rule(state: StateInline, silent: bool) -> bool:
149
152
return True
150
153
151
154
155
+ def _attr_inline_rule (
156
+ state : StateInline ,
157
+ silent : bool ,
158
+ after : Sequence [str ],
159
+ * ,
160
+ allowed : set [str ] | None = None ,
161
+ ) -> bool :
162
+ if state .pending or not state .tokens :
163
+ return False
164
+ token = state .tokens [- 1 ]
165
+ if token .type not in after :
166
+ return False
167
+ try :
168
+ new_pos , attrs = parse (state .src [state .pos :])
169
+ except ParseError :
170
+ return False
171
+ token_index = _find_opening (state .tokens , len (state .tokens ) - 1 )
172
+ if token_index is None :
173
+ return False
174
+ state .pos += new_pos + 1
175
+ if not silent :
176
+ attr_token = state .tokens [token_index ]
177
+ if "class" in attrs and "class" in token .attrs :
178
+ attrs ["class" ] = f"{ token .attrs ['class' ]} { attrs ['class' ]} "
179
+ _add_attrs (attr_token , attrs , allowed )
180
+ return True
181
+
182
+
152
183
def _attr_block_rule (
153
184
state : StateBlock , startLine : int , endLine : int , silent : bool
154
185
) -> bool :
@@ -197,7 +228,7 @@ def _attr_block_rule(
197
228
return True
198
229
199
230
200
- def _attr_resolve_block_rule (state : StateCore ) -> None :
231
+ def _attr_resolve_block_rule (state : StateCore , * , allowed : set [ str ] | None ) -> None :
201
232
"""Find attribute block then move its attributes to the next block."""
202
233
i = 0
203
234
len_tokens = len (state .tokens )
@@ -221,8 +252,23 @@ def _attr_resolve_block_rule(state: StateCore) -> None:
221
252
if key == "class" or key not in next_token .attrs :
222
253
next_token .attrs [key ] = value
223
254
else :
224
- # attribute block takes precedence over attributes in other blocks
225
- next_token .attrs .update (state .tokens [i ].attrs )
255
+ _add_attrs (next_token , state .tokens [i ].attrs , allowed )
226
256
227
257
state .tokens .pop (i )
228
258
len_tokens -= 1
259
+
260
+
261
+ def _add_attrs (
262
+ token : Token ,
263
+ attrs : dict [str , Any ],
264
+ allowed : set [str ] | None ,
265
+ ) -> None :
266
+ """Add attributes to a token, skipping any disallowed attributes."""
267
+ if allowed is not None and (
268
+ disallowed := {k : v for k , v in attrs .items () if k not in allowed }
269
+ ):
270
+ token .meta ["insecure_attrs" ] = disallowed
271
+ attrs = {k : v for k , v in attrs .items () if k in allowed }
272
+
273
+ # attributes takes precedence over existing attributes
274
+ token .attrs .update (attrs )
0 commit comments