1+ from typing import List , Optional
2+
13from markdown_it import MarkdownIt
24from markdown_it .rules_inline import StateInline
5+ from markdown_it .token import Token
36
47from .parse import ParseError , parse
58
69
7- def attrs_plugin (md : MarkdownIt , * , after = ("image" , "code_inline" )):
10+ def attrs_plugin (
11+ md : MarkdownIt ,
12+ * ,
13+ after = ("image" , "code_inline" , "link_close" , "span_close" ),
14+ spans = True ,
15+ ):
816 """Parse inline attributes that immediately follow certain inline elements::
917
1018 {#id .a b=c}
1119
20+ This syntax is inspired by
21+ `Djot spans
22+ <https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes>`_.
23+
1224 Inside the curly braces, the following syntax is possible:
1325
1426 - `.foo` specifies foo as a class.
@@ -22,14 +34,18 @@ def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")):
2234 Backslash escapes may be used inside quoted values.
2335 - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).
2436
25- **Note:** This plugin is currently limited to "self-closing" elements,
26- such as images and code spans. It does not work with links or emphasis.
37+ Multiple attribute blocks are merged.
2738
2839 :param md: The MarkdownIt instance to modify.
2940 :param after: The names of inline elements after which attributes may be specified.
41+ This plugin does not support attributes after emphasis, strikethrough or text elements,
42+ which all require post-parse processing.
43+ :param spans: If True, also parse attributes after spans of text, encapsulated by `[]`.
44+ Note Markdown link references take precedence over this syntax.
45+
3046 """
3147
32- def attr_rule (state : StateInline , silent : bool ):
48+ def _attr_rule (state : StateInline , silent : bool ):
3349 if state .pending or not state .tokens :
3450 return False
3551 token = state .tokens [- 1 ]
@@ -39,12 +55,64 @@ def attr_rule(state: StateInline, silent: bool):
3955 new_pos , attrs = parse (state .src [state .pos :])
4056 except ParseError :
4157 return False
58+ token_index = _find_opening (state .tokens , len (state .tokens ) - 1 )
59+ if token_index is None :
60+ return False
4261 state .pos += new_pos + 1
4362 if not silent :
63+ attr_token = state .tokens [token_index ]
4464 if "class" in attrs and "class" in token .attrs :
45- attrs ["class" ] = f"{ token .attrs ['class' ]} { attrs ['class' ]} "
46- token .attrs .update (attrs )
47-
65+ attrs ["class" ] = f"{ attr_token .attrs ['class' ]} { attrs ['class' ]} "
66+ attr_token .attrs .update (attrs )
4867 return True
4968
50- md .inline .ruler .push ("attr" , attr_rule )
69+ if spans :
70+ md .inline .ruler .after ("link" , "span" , _span_rule )
71+ md .inline .ruler .push ("attr" , _attr_rule )
72+
73+
74+ def _find_opening (tokens : List [Token ], index : int ) -> Optional [int ]:
75+ """Find the opening token index, if the token is closing."""
76+ if tokens [index ].nesting != - 1 :
77+ return index
78+ level = 0
79+ while index >= 0 :
80+ level += tokens [index ].nesting
81+ if level == 0 :
82+ return index
83+ index -= 1
84+ return None
85+
86+
87+ def _span_rule (state : StateInline , silent : bool ):
88+ if state .srcCharCode [state .pos ] != 0x5B : # /* [ */
89+ return False
90+
91+ maximum = state .posMax
92+ labelStart = state .pos + 1
93+ labelEnd = state .md .helpers .parseLinkLabel (state , state .pos , False )
94+
95+ # parser failed to find ']', so it's not a valid span
96+ if labelEnd < 0 :
97+ return False
98+
99+ pos = labelEnd + 1
100+
101+ try :
102+ new_pos , attrs = parse (state .src [pos :])
103+ except ParseError :
104+ return False
105+
106+ pos += new_pos + 1
107+
108+ if not silent :
109+ state .pos = labelStart
110+ state .posMax = labelEnd
111+ token = state .push ("span_open" , "span" , 1 )
112+ token .attrs = attrs
113+ state .md .inline .tokenize (state )
114+ token = state .push ("span_close" , "span" , - 1 )
115+
116+ state .pos = pos
117+ state .posMax = maximum
118+ return True
0 commit comments