22
33from __future__ import annotations
44
5- from typing import TYPE_CHECKING , Sequence
5+ from functools import partial
6+ from typing import TYPE_CHECKING , Sequence , TypedDict
67
78from markdown_it import MarkdownIt
89from markdown_it .helpers import parseLinkLabel
1819 from markdown_it .utils import EnvType , OptionsDict
1920
2021
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 :
2229 """Plugin ported from
2330 `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.
2431
@@ -38,13 +45,22 @@ def footnote_plugin(md: MarkdownIt) -> None:
3845 Subsequent paragraphs are indented to show that they
3946 belong to the previous footnote.
4047
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+
4152 """
4253 md .block .ruler .before (
4354 "reference" , "footnote_def" , footnote_def , {"alt" : ["paragraph" , "reference" ]}
4455 )
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 )
4864
4965 md .add_render_rule ("footnote_ref" , render_footnote_ref )
5066 md .add_render_rule ("footnote_block_open" , render_footnote_block_open )
@@ -58,6 +74,29 @@ def footnote_plugin(md: MarkdownIt) -> None:
5874 md .add_render_rule ("footnote_anchor_name" , render_footnote_anchor_name )
5975
6076
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+
61100# ## RULES ##
62101
63102
@@ -97,7 +136,8 @@ def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool)
97136 pos += 1
98137
99138 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
101141
102142 open_token = Token ("footnote_reference_open" , "" , 1 )
103143 open_token .meta = {"label" : label }
@@ -182,7 +222,7 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
182222 # so all that's left to do is to call tokenizer.
183223 #
184224 if not silent :
185- refs = state .env . setdefault ( "footnotes" , {}). setdefault ( "list" , {})
225+ refs = _data_from_env ( state .env )[ "list" ]
186226 footnoteId = len (refs )
187227
188228 tokens : list [Token ] = []
@@ -200,7 +240,9 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
200240 return True
201241
202242
203- def footnote_ref (state : StateInline , silent : bool ) -> bool :
243+ def footnote_ref (
244+ state : StateInline , silent : bool , * , always_match : bool = False
245+ ) -> bool :
204246 """Process footnote references ([^...])"""
205247
206248 maximum = state .posMax
@@ -210,7 +252,9 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
210252 if start + 3 > maximum :
211253 return False
212254
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" ]):
214258 return False
215259 if state .src [start ] != "[" :
216260 return False
@@ -219,9 +263,7 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
219263
220264 pos = start + 2
221265 while pos < maximum :
222- if state .src [pos ] == " " :
223- return False
224- if state .src [pos ] == "\n " :
266+ if state .src [pos ] in (" " , "\n " ):
225267 return False
226268 if state .src [pos ] == "]" :
227269 break
@@ -234,22 +276,19 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
234276 pos += 1
235277
236278 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 :
238280 return False
239281
240282 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
248287 else :
249- footnoteId = state . env [ "footnotes" ] ["refs" ][":" + label ]
288+ footnoteId = footnote_data ["refs" ][":" + label ]
250289
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
253292
254293 token = state .push ("footnote_ref" , "" , 0 )
255294 token .meta = {"id" : footnoteId , "subId" : footnoteSubId , "label" : label }
@@ -295,14 +334,14 @@ def footnote_tail(state: StateCore) -> None:
295334
296335 state .tokens = [t for t , f in zip (state .tokens , tok_filter ) if f ]
297336
298- if "list" not in state .env .get ("footnotes" , {}):
337+ footnote_data = _data_from_env (state .env )
338+ if not footnote_data ["list" ]:
299339 return
300- foot_list = state .env ["footnotes" ]["list" ]
301340
302341 token = Token ("footnote_block_open" , "" , 1 )
303342 state .tokens .append (token )
304343
305- for i , foot_note in foot_list .items ():
344+ for i , foot_note in footnote_data [ "list" ] .items ():
306345 token = Token ("footnote_open" , "" , 1 )
307346 token .meta = {"id" : i , "label" : foot_note .get ("label" , None )}
308347 # TODO propagate line positions of original foot note
@@ -326,7 +365,7 @@ def footnote_tail(state: StateCore) -> None:
326365 tokens .append (token )
327366
328367 elif "label" in foot_note :
329- tokens = refTokens [ ":" + foot_note ["label" ]]
368+ tokens = refTokens . get ( ":" + foot_note ["label" ], [])
330369
331370 state .tokens .extend (tokens )
332371 if state .tokens [len (state .tokens ) - 1 ].type == "paragraph_close" :
0 commit comments