Skip to content

Commit 8afeaaf

Browse files
facelessuserwaylan
authored andcommitted
Create additional references for duplicate footnotes (#534)
Track when we find duplicate footnote references and create unique ids for them. Then add an additional tree-processor after inline to go back and update the footnotes with additional back references that link to the duplicate footnote references. Fixes #468.
1 parent 594b25d commit 8afeaaf

File tree

3 files changed

+115
-5
lines changed

3 files changed

+115
-5
lines changed

markdown/extensions/footnotes.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
from ..util import etree, text_type
2424
from ..odict import OrderedDict
2525
import re
26+
import copy
2627

2728
FN_BACKLINK_TEXT = "zz1337820767766393qq"
2829
NBSP_PLACEHOLDER = "qq3936677670287331zz"
2930
DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)')
3031
TABBED_RE = re.compile(r'((\t)|( ))(.*)')
32+
RE_REF_ID = re.compile(r'(fnref)(\d+)')
3133

3234

3335
class FootnoteExtension(Extension):
@@ -53,6 +55,8 @@ def __init__(self, *args, **kwargs):
5355

5456
# In multiple invocations, emit links that don't get tangled.
5557
self.unique_prefix = 0
58+
self.found_refs = {}
59+
self.used_refs = set()
5660

5761
self.reset()
5862

@@ -76,6 +80,15 @@ def extendMarkdown(self, md, md_globals):
7680
md.treeprocessors.add(
7781
"footnote", FootnoteTreeprocessor(self), "_begin"
7882
)
83+
84+
# Insert a tree-processor that will run after inline is done.
85+
# In this tree-processor we want to check our duplicate footnote tracker
86+
# And add additional backrefs to the footnote pointing back to the
87+
# duplicated references.
88+
md.treeprocessors.add(
89+
"footnote-duplicate", FootnotePostTreeprocessor(self), '>inline'
90+
)
91+
7992
# Insert a postprocessor after amp_substitute oricessor
8093
md.postprocessors.add(
8194
"footnote", FootnotePostprocessor(self), ">amp_substitute"
@@ -85,6 +98,29 @@ def reset(self):
8598
""" Clear footnotes on reset, and prepare for distinct document. """
8699
self.footnotes = OrderedDict()
87100
self.unique_prefix += 1
101+
self.found_refs = {}
102+
self.used_refs = set()
103+
104+
def unique_ref(self, reference, found=False):
105+
""" Get a unique reference if there are duplicates. """
106+
if not found:
107+
return reference
108+
109+
original_ref = reference
110+
while reference in self.used_refs:
111+
ref, rest = reference.split(self.get_separator(), 1)
112+
m = RE_REF_ID.match(ref)
113+
if m:
114+
reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
115+
else:
116+
reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
117+
118+
self.used_refs.add(reference)
119+
if original_ref in self.found_refs:
120+
self.found_refs[original_ref] += 1
121+
else:
122+
self.found_refs[original_ref] = 1
123+
return reference
88124

89125
def findFootnotesPlaceholder(self, root):
90126
""" Return ElementTree Element that contains Footnote placeholder. """
@@ -120,13 +156,12 @@ def makeFootnoteId(self, id):
120156
else:
121157
return 'fn%s%s' % (self.get_separator(), id)
122158

123-
def makeFootnoteRefId(self, id):
159+
def makeFootnoteRefId(self, id, found=False):
124160
""" Return footnote back-link id. """
125161
if self.getConfig("UNIQUE_IDS"):
126-
return 'fnref%s%d-%s' % (self.get_separator(),
127-
self.unique_prefix, id)
162+
return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
128163
else:
129-
return 'fnref%s%s' % (self.get_separator(), id)
164+
return self.unique_ref('fnref%s%s' % (self.get_separator(), id), found)
130165

131166
def makeFootnotesDiv(self, root):
132167
""" Return div of footnotes as et Element. """
@@ -270,7 +305,7 @@ def handleMatch(self, m):
270305
if id in self.footnotes.footnotes.keys():
271306
sup = etree.Element("sup")
272307
a = etree.SubElement(sup, "a")
273-
sup.set('id', self.footnotes.makeFootnoteRefId(id))
308+
sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
274309
a.set('href', '#' + self.footnotes.makeFootnoteId(id))
275310
if self.footnotes.md.output_format not in ['html5', 'xhtml5']:
276311
a.set('rel', 'footnote') # invalid in HTML5
@@ -281,6 +316,59 @@ def handleMatch(self, m):
281316
return None
282317

283318

319+
class FootnotePostTreeprocessor(Treeprocessor):
320+
""" Ammend footnote div with duplicates. """
321+
322+
def __init__(self, footnotes):
323+
self.footnotes = footnotes
324+
325+
def add_duplicates(self, li, duplicates):
326+
""" Adjust current li and add the duplicates: fnref2, fnref3, etc. """
327+
for link in li.iter('a'):
328+
# Find the link that needs to be duplicated.
329+
if link.attrib.get('class', '') == 'footnote-backref':
330+
ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
331+
# Duplicate link the number of times we need to
332+
# and point the to the appropriate references.
333+
links = []
334+
for index in range(2, duplicates + 1):
335+
sib_link = copy.deepcopy(link)
336+
sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
337+
links.append(sib_link)
338+
self.offset += 1
339+
# Add all the new duplicate links.
340+
el = list(li)[-1]
341+
for l in links:
342+
el.append(l)
343+
break
344+
345+
def get_num_duplicates(self, li):
346+
""" Get the number of duplicate refs of the footnote. """
347+
fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
348+
link_id = '%sref%s%s' % (fn, self.footnotes.get_separator(), rest)
349+
return self.footnotes.found_refs.get(link_id, 0)
350+
351+
def handle_duplicates(self, parent):
352+
""" Find duplicate footnotes and format and add the duplicates. """
353+
for li in list(parent):
354+
# Check number of duplicates footnotes and insert
355+
# additional links if needed.
356+
count = self.get_num_duplicates(li)
357+
if count > 1:
358+
self.add_duplicates(li, count)
359+
360+
def run(self, root):
361+
""" Crawl the footnote div and add missing duplicate footnotes. """
362+
self.offset = 0
363+
for div in root.iter('div'):
364+
if div.attrib.get('class', '') == 'footnote':
365+
# Footnotes shoul be under the first orderd list under
366+
# the footnote div. So once we find it, quit.
367+
for ol in div.iter('ol'):
368+
self.handle_duplicates(ol)
369+
break
370+
371+
284372
class FootnoteTreeprocessor(Treeprocessor):
285373
""" Build and append footnote div to end of document. """
286374

tests/extensions/extra/footnote.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<p>This is the body with a footnote<sup id="fnref:1"><a class="footnote-ref" href="#fn:1" rel="footnote">1</a></sup> or two<sup id="fnref:2"><a class="footnote-ref" href="#fn:2" rel="footnote">2</a></sup> or more<sup id="fnref:3"><a class="footnote-ref" href="#fn:3" rel="footnote">3</a></sup> <sup id="fnref:4"><a class="footnote-ref" href="#fn:4" rel="footnote">4</a></sup> <sup id="fnref:5"><a class="footnote-ref" href="#fn:5" rel="footnote">5</a></sup>.</p>
22
<p>Also a reference that does not exist[^6].</p>
3+
<p>Duplicate<sup id="fnref:a"><a class="footnote-ref" href="#fn:a" rel="footnote">6</a></sup> footnotes<sup id="fnref2:a"><a class="footnote-ref" href="#fn:a" rel="footnote">6</a></sup> test<sup id="fnref3:a"><a class="footnote-ref" href="#fn:a" rel="footnote">6</a></sup>.</p>
4+
<p>Duplicate<sup id="fnref:b"><a class="footnote-ref" href="#fn:b" rel="footnote">7</a></sup> footnotes<sup id="fnref2:b"><a class="footnote-ref" href="#fn:b" rel="footnote">7</a></sup> test<sup id="fnref3:b"><a class="footnote-ref" href="#fn:b" rel="footnote">7</a></sup>.</p>
5+
<p>Single after duplicates<sup id="fnref:c"><a class="footnote-ref" href="#fn:c" rel="footnote">8</a></sup>.</p>
36
<div class="footnote">
47
<hr />
58
<ol>
@@ -29,5 +32,14 @@
2932
Second line of first paragraph is not intended.
3033
Nor is third...&#160;<a class="footnote-backref" href="#fnref:5" rev="footnote" title="Jump back to footnote 5 in the text">&#8617;</a></p>
3134
</li>
35+
<li id="fn:a">
36+
<p>1&#160;<a class="footnote-backref" href="#fnref:a" rev="footnote" title="Jump back to footnote 6 in the text">&#8617;</a><a class="footnote-backref" href="#fnref2:a" rev="footnote" title="Jump back to footnote 6 in the text">&#8617;</a><a class="footnote-backref" href="#fnref3:a" rev="footnote" title="Jump back to footnote 6 in the text">&#8617;</a></p>
37+
</li>
38+
<li id="fn:b">
39+
<p>2&#160;<a class="footnote-backref" href="#fnref:b" rev="footnote" title="Jump back to footnote 7 in the text">&#8617;</a><a class="footnote-backref" href="#fnref2:b" rev="footnote" title="Jump back to footnote 7 in the text">&#8617;</a><a class="footnote-backref" href="#fnref3:b" rev="footnote" title="Jump back to footnote 7 in the text">&#8617;</a></p>
40+
</li>
41+
<li id="fn:c">
42+
<p>3&#160;<a class="footnote-backref" href="#fnref:c" rev="footnote" title="Jump back to footnote 8 in the text">&#8617;</a></p>
43+
</li>
3244
</ol>
3345
</div>

tests/extensions/extra/footnote.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ This is the body with a footnote[^1] or two[^2] or more[^3] [^4] [^5].
22

33
Also a reference that does not exist[^6].
44

5+
Duplicate[^a] footnotes[^a] test[^a].
6+
7+
Duplicate[^b] footnotes[^b] test[^b].
8+
9+
Single after duplicates[^c].
10+
511
[^1]: Footnote that ends with a list:
612

713
* item 1
@@ -18,3 +24,7 @@ Also a reference that does not exist[^6].
1824
[^5]: First line of first paragraph.
1925
Second line of first paragraph is not intended.
2026
Nor is third...
27+
28+
[^a]: 1
29+
[^b]: 2
30+
[^c]: 3

0 commit comments

Comments
 (0)