2
2
3
3
from __future__ import annotations
4
4
5
- from typing import TYPE_CHECKING , Sequence
5
+ from functools import partial
6
+ from typing import TYPE_CHECKING , Sequence , TypedDict
6
7
7
8
from markdown_it import MarkdownIt
8
9
from markdown_it .helpers import parseLinkLabel
18
19
from markdown_it .utils import EnvType , OptionsDict
19
20
20
21
21
- def footnote_plugin (md : MarkdownIt ) -> None :
22
+ def footnote_plugin (
23
+ md : MarkdownIt ,
24
+ * ,
25
+ inline : bool = True ,
26
+ move_to_end : bool = True ,
27
+ always_match_refs : bool = False ,
28
+ ) -> None :
22
29
"""Plugin ported from
23
30
`markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.
24
31
@@ -38,13 +45,22 @@ def footnote_plugin(md: MarkdownIt) -> None:
38
45
Subsequent paragraphs are indented to show that they
39
46
belong to the previous footnote.
40
47
48
+ :param inline: If True, also parse inline footnotes (^[...]).
49
+ :param move_to_end: If True, move footnote definitions to the end of the token stream.
50
+ :param always_match_refs: If True, match references, even if the footnote is not defined.
51
+
41
52
"""
42
53
md .block .ruler .before (
43
54
"reference" , "footnote_def" , footnote_def , {"alt" : ["paragraph" , "reference" ]}
44
55
)
45
- md .inline .ruler .after ("image" , "footnote_inline" , footnote_inline )
46
- md .inline .ruler .after ("footnote_inline" , "footnote_ref" , footnote_ref )
47
- md .core .ruler .after ("inline" , "footnote_tail" , footnote_tail )
56
+ _footnote_ref = partial (footnote_ref , always_match = always_match_refs )
57
+ if inline :
58
+ md .inline .ruler .after ("image" , "footnote_inline" , footnote_inline )
59
+ md .inline .ruler .after ("footnote_inline" , "footnote_ref" , _footnote_ref )
60
+ else :
61
+ md .inline .ruler .after ("image" , "footnote_ref" , _footnote_ref )
62
+ if move_to_end :
63
+ md .core .ruler .after ("inline" , "footnote_tail" , footnote_tail )
48
64
49
65
md .add_render_rule ("footnote_ref" , render_footnote_ref )
50
66
md .add_render_rule ("footnote_block_open" , render_footnote_block_open )
@@ -58,6 +74,29 @@ def footnote_plugin(md: MarkdownIt) -> None:
58
74
md .add_render_rule ("footnote_anchor_name" , render_footnote_anchor_name )
59
75
60
76
77
+ class _RefData (TypedDict , total = False ):
78
+ # standard
79
+ label : str
80
+ count : int
81
+ # inline
82
+ content : str
83
+ tokens : list [Token ]
84
+
85
+
86
+ class _FootnoteData (TypedDict ):
87
+ refs : dict [str , int ]
88
+ """A mapping of all footnote labels (prefixed with ``:``) to their ID (-1 if not yet set)."""
89
+ list : dict [int , _RefData ]
90
+ """A mapping of all footnote IDs to their data."""
91
+
92
+
93
+ def _data_from_env (env : EnvType ) -> _FootnoteData :
94
+ footnotes = env .setdefault ("footnotes" , {})
95
+ footnotes .setdefault ("refs" , {})
96
+ footnotes .setdefault ("list" , {})
97
+ return footnotes # type: ignore[no-any-return]
98
+
99
+
61
100
# ## RULES ##
62
101
63
102
@@ -97,7 +136,8 @@ def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool)
97
136
pos += 1
98
137
99
138
label = state .src [start + 2 : pos - 2 ]
100
- state .env .setdefault ("footnotes" , {}).setdefault ("refs" , {})[":" + label ] = - 1
139
+ footnote_data = _data_from_env (state .env )
140
+ footnote_data ["refs" ][":" + label ] = - 1
101
141
102
142
open_token = Token ("footnote_reference_open" , "" , 1 )
103
143
open_token .meta = {"label" : label }
@@ -182,7 +222,7 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
182
222
# so all that's left to do is to call tokenizer.
183
223
#
184
224
if not silent :
185
- refs = state .env . setdefault ( "footnotes" , {}). setdefault ( "list" , {})
225
+ refs = _data_from_env ( state .env )[ "list" ]
186
226
footnoteId = len (refs )
187
227
188
228
tokens : list [Token ] = []
@@ -200,7 +240,9 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
200
240
return True
201
241
202
242
203
- def footnote_ref (state : StateInline , silent : bool ) -> bool :
243
+ def footnote_ref (
244
+ state : StateInline , silent : bool , * , always_match : bool = False
245
+ ) -> bool :
204
246
"""Process footnote references ([^...])"""
205
247
206
248
maximum = state .posMax
@@ -210,7 +252,9 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
210
252
if start + 3 > maximum :
211
253
return False
212
254
213
- if "footnotes" not in state .env or "refs" not in state .env ["footnotes" ]:
255
+ footnote_data = _data_from_env (state .env )
256
+
257
+ if not (always_match or footnote_data ["refs" ]):
214
258
return False
215
259
if state .src [start ] != "[" :
216
260
return False
@@ -219,9 +263,7 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
219
263
220
264
pos = start + 2
221
265
while pos < maximum :
222
- if state .src [pos ] == " " :
223
- return False
224
- if state .src [pos ] == "\n " :
266
+ if state .src [pos ] in (" " , "\n " ):
225
267
return False
226
268
if state .src [pos ] == "]" :
227
269
break
@@ -234,22 +276,19 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
234
276
pos += 1
235
277
236
278
label = state .src [start + 2 : pos - 1 ]
237
- if (":" + label ) not in state . env [ "footnotes" ][ " refs" ]:
279
+ if (( ":" + label ) not in footnote_data [ " refs" ]) and not always_match :
238
280
return False
239
281
240
282
if not silent :
241
- if "list" not in state .env ["footnotes" ]:
242
- state .env ["footnotes" ]["list" ] = {}
243
-
244
- if state .env ["footnotes" ]["refs" ][":" + label ] < 0 :
245
- footnoteId = len (state .env ["footnotes" ]["list" ])
246
- state .env ["footnotes" ]["list" ][footnoteId ] = {"label" : label , "count" : 0 }
247
- state .env ["footnotes" ]["refs" ][":" + label ] = footnoteId
283
+ if footnote_data ["refs" ].get (":" + label , - 1 ) < 0 :
284
+ footnoteId = len (footnote_data ["list" ])
285
+ footnote_data ["list" ][footnoteId ] = {"label" : label , "count" : 0 }
286
+ footnote_data ["refs" ][":" + label ] = footnoteId
248
287
else :
249
- footnoteId = state . env [ "footnotes" ] ["refs" ][":" + label ]
288
+ footnoteId = footnote_data ["refs" ][":" + label ]
250
289
251
- footnoteSubId = state . env [ "footnotes" ] ["list" ][footnoteId ]["count" ]
252
- state . env [ "footnotes" ] ["list" ][footnoteId ]["count" ] += 1
290
+ footnoteSubId = footnote_data ["list" ][footnoteId ]["count" ]
291
+ footnote_data ["list" ][footnoteId ]["count" ] += 1
253
292
254
293
token = state .push ("footnote_ref" , "" , 0 )
255
294
token .meta = {"id" : footnoteId , "subId" : footnoteSubId , "label" : label }
@@ -295,14 +334,14 @@ def footnote_tail(state: StateCore) -> None:
295
334
296
335
state .tokens = [t for t , f in zip (state .tokens , tok_filter ) if f ]
297
336
298
- if "list" not in state .env .get ("footnotes" , {}):
337
+ footnote_data = _data_from_env (state .env )
338
+ if not footnote_data ["list" ]:
299
339
return
300
- foot_list = state .env ["footnotes" ]["list" ]
301
340
302
341
token = Token ("footnote_block_open" , "" , 1 )
303
342
state .tokens .append (token )
304
343
305
- for i , foot_note in foot_list .items ():
344
+ for i , foot_note in footnote_data [ "list" ] .items ():
306
345
token = Token ("footnote_open" , "" , 1 )
307
346
token .meta = {"id" : i , "label" : foot_note .get ("label" , None )}
308
347
# TODO propagate line positions of original foot note
@@ -326,7 +365,7 @@ def footnote_tail(state: StateCore) -> None:
326
365
tokens .append (token )
327
366
328
367
elif "label" in foot_note :
329
- tokens = refTokens [ ":" + foot_note ["label" ]]
368
+ tokens = refTokens . get ( ":" + foot_note ["label" ], [])
330
369
331
370
state .tokens .extend (tokens )
332
371
if state .tokens [len (state .tokens ) - 1 ].type == "paragraph_close" :
0 commit comments