Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .spell-dict
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ GSoC
hacky
HeaderId
HTTPS
href
html
implementers
InlineProcessor
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ See the [Contributing Guide](contributing.md) for details.

### Fixed

* Number nested-block footnotes in document order when `USE_DEFINITION_ORDER` is `False` (#1561).
* Fix an issue with excessive backtracking when matching inline code blocks (#1617).

## [3.10.3] - 2026-07-30
Expand Down
31 changes: 31 additions & 0 deletions markdown/extensions/footnotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,12 +448,43 @@ def __init__(self, footnotes: FootnoteExtension):
def run(self, root: etree.Element) -> None:
if not self.footnotes.footnotes:
return
# Rebuild from the tree: inline processing is not document order.
self.footnotes.footnote_order = self.get_document_order(root)
self.renumber_refs(root)
if self.footnotes.footnote_order != list(self.footnotes.footnotes.keys()):
for div in root.iter('div'):
if div.attrib.get('class', '') == 'footnote':
self.reorder_footnotes(div)
break

def get_fn_id(self, href: str) -> str:
""" Return the footnote id from a `footnote-ref` href. """
return href.lstrip('#').split(self.footnotes.get_separator(), 1)[-1]

def get_document_order(self, root: etree.Element) -> list[str]:
""" Return footnote ids in document order (first reference wins). """
order: list[str] = []
for el in root.iter('a'):
if el.attrib.get('class', '') != 'footnote-ref':
continue
fn_id = self.get_fn_id(el.attrib.get('href', ''))
if fn_id not in order:
order.append(fn_id)
return order

def renumber_refs(self, root: etree.Element) -> None:
""" Rewrite superscript numbers to match document order. """
numbers = {
fn_id: i for i, fn_id in enumerate(self.footnotes.footnote_order, start=1)
}
fmt = self.footnotes.getConfig("SUPERSCRIPT_TEXT")
for el in root.iter('a'):
if el.attrib.get('class', '') != 'footnote-ref':
continue
fn_id = self.get_fn_id(el.attrib.get('href', ''))
if fn_id in numbers:
el.text = fmt.format(numbers[fn_id])

def reorder_footnotes(self, parent: etree.Element) -> None:
old_list = parent.find('ol')
parent.remove(old_list)
Expand Down
102 changes: 102 additions & 0 deletions tests/test_syntax/extensions/test_footnotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,108 @@ def test_footnote_order_by_definition(self):
extension_configs={'footnotes': {'USE_DEFINITION_ORDER': True}}
)

def test_footnote_order_nested_blocks(self):
"""Test document-order numbering when refs are inside nested blocks."""

self.assertMarkdownRenders(
self.dedent(
"""
First.[^1]

1. Nested list.[^2]

Third.[^3]

> Nested quote.[^4]

Fifth.[^5]

[^1]: First
[^2]: Second
[^3]: Third
[^4]: Fourth
[^5]: Fifth
"""
),
'<p>First.<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup></p>\n'
'<ol>\n'
'<li>Nested list.<sup id="fnref:2"><a class="footnote-ref" href="#fn:2">2</a></sup></li>\n'
'</ol>\n'
'<p>Third.<sup id="fnref:3"><a class="footnote-ref" href="#fn:3">3</a></sup></p>\n'
'<blockquote>\n'
'<p>Nested quote.<sup id="fnref:4"><a class="footnote-ref" href="#fn:4">4</a></sup></p>\n'
'</blockquote>\n'
'<p>Fifth.<sup id="fnref:5"><a class="footnote-ref" href="#fn:5">5</a></sup></p>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:1">\n'
'<p>First&#160;<a class="footnote-backref" href="#fnref:1"'
' title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:2">\n'
'<p>Second&#160;<a class="footnote-backref" href="#fnref:2"'
' title="Jump back to footnote 2 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:3">\n'
'<p>Third&#160;<a class="footnote-backref" href="#fnref:3"'
' title="Jump back to footnote 3 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:4">\n'
'<p>Fourth&#160;<a class="footnote-backref" href="#fnref:4"'
' title="Jump back to footnote 4 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:5">\n'
'<p>Fifth&#160;<a class="footnote-backref" href="#fnref:5"'
' title="Jump back to footnote 5 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>',
extension_configs={'footnotes': {'USE_DEFINITION_ORDER': False}}
)

def test_footnote_order_nested_blocks_not_definition_order(self):
"""Test that nested refs follow document order, not definition order."""

self.assertMarkdownRenders(
self.dedent(
"""
First.[^b]

1. Nested.[^a]

Third.[^c]

[^c]: C
[^a]: A
[^b]: B
"""
),
'<p>First.<sup id="fnref:b"><a class="footnote-ref" href="#fn:b">1</a></sup></p>\n'
'<ol>\n'
'<li>Nested.<sup id="fnref:a"><a class="footnote-ref" href="#fn:a">2</a></sup></li>\n'
'</ol>\n'
'<p>Third.<sup id="fnref:c"><a class="footnote-ref" href="#fn:c">3</a></sup></p>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:b">\n'
'<p>B&#160;<a class="footnote-backref" href="#fnref:b"'
' title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:a">\n'
'<p>A&#160;<a class="footnote-backref" href="#fnref:a"'
' title="Jump back to footnote 2 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:c">\n'
'<p>C&#160;<a class="footnote-backref" href="#fnref:c"'
' title="Jump back to footnote 3 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>',
extension_configs={'footnotes': {'USE_DEFINITION_ORDER': False}}
)

def test_footnote_reference_within_code_span(self):
"""Test footnote reference within a code span."""

Expand Down
Loading