33
33
FN_BACKLINK_TEXT = util .STX + "zz1337820767766393qq" + util .ETX
34
34
NBSP_PLACEHOLDER = util .STX + "qq3936677670287331zz" + util .ETX
35
35
RE_REF_ID = re .compile (r'(fnref)(\d+)' )
36
+ RE_REFERENCE = re .compile (r'(?<!!)\[\^([^\]]*)\](?!\s*:)' )
36
37
37
38
38
39
class FootnoteExtension (Extension ):
@@ -61,6 +62,9 @@ def __init__(self, **kwargs):
61
62
],
62
63
'SEPARATOR' : [
63
64
':' , 'Footnote separator.'
65
+ ],
66
+ 'USE_DEFINITION_ORDER' : [
67
+ False , 'Whether to order footnotes by footnote content rather than by footnote label.'
64
68
]
65
69
}
66
70
""" Default configuration options. """
@@ -71,6 +75,9 @@ def __init__(self, **kwargs):
71
75
self .found_refs : dict [str , int ] = {}
72
76
self .used_refs : set [str ] = set ()
73
77
78
+ # Backward compatibility with old '%d' placeholder
79
+ self .setConfig ('BACKLINK_TITLE' , self .getConfig ("BACKLINK_TITLE" ).replace ("%d" , "{}" ))
80
+
74
81
self .reset ()
75
82
76
83
def extendMarkdown (self , md ):
@@ -89,6 +96,12 @@ def extendMarkdown(self, md):
89
96
# `codehilite`) so they can run on the the contents of the div.
90
97
md .treeprocessors .register (FootnoteTreeprocessor (self ), 'footnote' , 50 )
91
98
99
+ # Insert a tree-processor to reorder the footnotes if necessary. This must be after
100
+ # `inline` tree-processor so it can access the footnote reference order
101
+ # (`self.footnote_order`) that gets populated by the `FootnoteInlineProcessor`.
102
+ if not self .getConfig ("USE_DEFINITION_ORDER" ):
103
+ md .treeprocessors .register (FootnoteReorderingProcessor (self ), 'footnote-reorder' , 19 )
104
+
92
105
# Insert a tree-processor that will run after inline is done.
93
106
# In this tree-processor we want to check our duplicate footnote tracker
94
107
# And add additional `backrefs` to the footnote pointing back to the
@@ -100,6 +113,7 @@ def extendMarkdown(self, md):
100
113
101
114
def reset (self ) -> None :
102
115
""" Clear footnotes on reset, and prepare for distinct document. """
116
+ self .footnote_order : list [str ] = []
103
117
self .footnotes : OrderedDict [str , str ] = OrderedDict ()
104
118
self .unique_prefix += 1
105
119
self .found_refs = {}
@@ -150,6 +164,11 @@ def setFootnote(self, id: str, text: str) -> None:
150
164
""" Store a footnote for later retrieval. """
151
165
self .footnotes [id ] = text
152
166
167
+ def addFootnoteRef (self , id : str ) -> None :
168
+ """ Store a footnote reference id in order of appearance. """
169
+ if id not in self .footnote_order :
170
+ self .footnote_order .append (id )
171
+
153
172
def get_separator (self ) -> str :
154
173
""" Get the footnote separator. """
155
174
return self .getConfig ("SEPARATOR" )
@@ -180,9 +199,6 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None:
180
199
ol = etree .SubElement (div , "ol" )
181
200
surrogate_parent = etree .Element ("div" )
182
201
183
- # Backward compatibility with old '%d' placeholder
184
- backlink_title = self .getConfig ("BACKLINK_TITLE" ).replace ("%d" , "{}" )
185
-
186
202
for index , id in enumerate (self .footnotes .keys (), start = 1 ):
187
203
li = etree .SubElement (ol , "li" )
188
204
li .set ("id" , self .makeFootnoteId (id ))
@@ -198,7 +214,7 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None:
198
214
backlink .set ("class" , "footnote-backref" )
199
215
backlink .set (
200
216
"title" ,
201
- backlink_title .format (index )
217
+ self . getConfig ( 'BACKLINK_TITLE' ) .format (index )
202
218
)
203
219
backlink .text = FN_BACKLINK_TEXT
204
220
@@ -214,7 +230,7 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None:
214
230
215
231
216
232
class FootnoteBlockProcessor (BlockProcessor ):
217
- """ Find all footnote references and store for later use. """
233
+ """ Find footnote definitions and store for later use. """
218
234
219
235
RE = re .compile (r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$' , re .MULTILINE )
220
236
@@ -228,6 +244,7 @@ def test(self, parent: etree.Element, block: str) -> bool:
228
244
def run (self , parent : etree .Element , blocks : list [str ]) -> bool :
229
245
""" Find, set, and remove footnote definitions. """
230
246
block = blocks .pop (0 )
247
+
231
248
m = self .RE .search (block )
232
249
if m :
233
250
id = m .group (1 )
@@ -312,14 +329,21 @@ def __init__(self, pattern: str, footnotes: FootnoteExtension):
312
329
def handleMatch (self , m : re .Match [str ], data : str ) -> tuple [etree .Element | None , int | None , int | None ]:
313
330
id = m .group (1 )
314
331
if id in self .footnotes .footnotes .keys ():
332
+ self .footnotes .addFootnoteRef (id )
333
+
334
+ if not self .footnotes .getConfig ("USE_DEFINITION_ORDER" ):
335
+ # Order by reference
336
+ footnote_num = self .footnotes .footnote_order .index (id ) + 1
337
+ else :
338
+ # Order by definition
339
+ footnote_num = list (self .footnotes .footnotes .keys ()).index (id ) + 1
340
+
315
341
sup = etree .Element ("sup" )
316
342
a = etree .SubElement (sup , "a" )
317
343
sup .set ('id' , self .footnotes .makeFootnoteRefId (id , found = True ))
318
344
a .set ('href' , '#' + self .footnotes .makeFootnoteId (id ))
319
345
a .set ('class' , 'footnote-ref' )
320
- a .text = self .footnotes .getConfig ("SUPERSCRIPT_TEXT" ).format (
321
- list (self .footnotes .footnotes .keys ()).index (id ) + 1
322
- )
346
+ a .text = self .footnotes .getConfig ("SUPERSCRIPT_TEXT" ).format (footnote_num )
323
347
return sup , m .start (0 ), m .end (0 )
324
348
else :
325
349
return None , None , None
@@ -401,6 +425,44 @@ def run(self, root: etree.Element) -> None:
401
425
root .append (footnotesDiv )
402
426
403
427
428
+ class FootnoteReorderingProcessor (Treeprocessor ):
429
+ """ Reorder list items in the footnotes div. """
430
+
431
+ def __init__ (self , footnotes : FootnoteExtension ):
432
+ self .footnotes = footnotes
433
+
434
+ def run (self , root : etree .Element ) -> None :
435
+ if not self .footnotes .footnotes :
436
+ return
437
+ if self .footnotes .footnote_order != list (self .footnotes .footnotes .keys ()):
438
+ for div in root .iter ('div' ):
439
+ if div .attrib .get ('class' , '' ) == 'footnote' :
440
+ self .reorder_footnotes (div )
441
+ break
442
+
443
+ def reorder_footnotes (self , parent : etree .Element ) -> None :
444
+ old_list = parent .find ('ol' )
445
+ parent .remove (old_list )
446
+ items = old_list .findall ('li' )
447
+
448
+ def order_by_id (li ) -> int :
449
+ id = li .attrib .get ('id' , '' ).split (self .footnotes .get_separator (), 1 )[- 1 ]
450
+ return (
451
+ self .footnotes .footnote_order .index (id )
452
+ if id in self .footnotes .footnote_order
453
+ else len (self .footnotes .footnotes )
454
+ )
455
+
456
+ items = sorted (items , key = order_by_id )
457
+
458
+ new_list = etree .SubElement (parent , 'ol' )
459
+
460
+ for index , item in enumerate (items , start = 1 ):
461
+ backlink = item .find ('.//a[@class="footnote-backref"]' )
462
+ backlink .set ("title" , self .footnotes .getConfig ("BACKLINK_TITLE" ).format (index ))
463
+ new_list .append (item )
464
+
465
+
404
466
class FootnotePostprocessor (Postprocessor ):
405
467
""" Replace placeholders with html entities. """
406
468
def __init__ (self , footnotes : FootnoteExtension ):
0 commit comments