Skip to content

Fix footnote ordering #1546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

aeskildsen
Copy link

Footnotes are now displayed in the order they are referenced in the document text, not in the order they are defined.

  • Set up a separate list to keep track of footnote reference order
  • Reorder footnote dict before rendering the list of footnotes
  • Update docstrings to reflect chagnes

Tracking footnote references is something the logically fits within inline processing. But it must happen before tree processing, because that is where the list of footnotes is rendered to html. For that reason, it is done within the block processor.

Fixes #1367

@waylan
Copy link
Member

waylan commented Jul 21, 2025

The test failures for the py314 test can be ignored as they are unrelated. I have opened #1547 to address them.

@waylan
Copy link
Member

waylan commented Jul 21, 2025

Tracking footnote references is something the logically fits within inline processing. But it must happen before tree processing, because that is where the list of footnotes is rendered to html. For that reason, it is done within the block processor.

While I understand that it may be easier to implement this way, I am unconvinced that it is imposable to implement in the inline processor. My concern is that it may match things it shouldn't. For example, what if a footnote is included in a code span? Some tests might convince me.

In fact, this needs some new tests added to demonstrate that it is working as advertised.

@aeskildsen
Copy link
Author

Thank you for the quick feedback. The code span example is definitely a valid concern.

I can add some tests to demonstrate the functionality + a footnote reference inside a code span.

I'll think about how to implement this with the inline processor. The problem is not detecting footnote references in the inline processor, it's the timing, since the tree processor which renders the footnotes runs before that.

@aeskildsen
Copy link
Author

aeskildsen commented Jul 22, 2025

I've added a bunch of tests, including footnote references within code spans. See in particular TestFootnotes.testFootnoteOrder.

Concerning the block vs inline discussion: One consideration I would to add is that my implementation does not remove or modify any of the footnote references/labels during block processing. It just registers the ids of any footnote references it finds in the blocks. I have not touched the parts of the code that remove substrings or render html elements. I merely reorder the dict that holds the ids and footnote content just before the footnotes are rendered.

@waylan
Copy link
Member

waylan commented Jul 23, 2025

Sorry that I didn't mention this previously, but all new tests should be under the appropriate subdir of /tests/test_syntax (/tests/test_syntax/extensions.test_footnotes.py in this case) and make use of markdown.test_tools.TestCase when it makes sense to do so.

- Move new tests to proper file (tests/test_syntax/extensions/test_footnotes.py)
- Adapt new tests to use markdown.test_tools.TestCase and style convention in test_footnotes.py
- Remove a few of the new tests which turned out to be redundant, given the existing ones in test_footnotes.py
@aeskildsen
Copy link
Author

Ah, I see. No worries, I've moved and adapted the new tests.

Comment on lines +340 to +362
def test_footnote_order(self):
"""Test that footnotes occur in order of reference appearance."""

self.assertMarkdownRenders(
'First footnote reference[^first]. Second footnote reference[^last].\n\n'
'[^last]: Second footnote.\n[^first]: First footnote.',
'<p>First footnote reference<sup id="fnref:first"><a class="footnote-ref" '
'href="#fn:first">1</a></sup>. Second footnote reference<sup id="fnref:last">'
'<a class="footnote-ref" href="#fn:last">2</a></sup>.</p>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:first">\n'
'<p>First footnote.&#160;<a class="footnote-backref" href="#fnref:first" '
'title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:last">\n'
'<p>Second footnote.&#160;<a class="footnote-backref" href="#fnref:last" '
'title="Jump back to footnote 2 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>'
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests the new behavior (footnotes ordered by reference appearence).

Comment on lines +364 to +462
def test_footnote_reference_within_code_span(self):
"""Test footnote reference within a code span."""

self.assertMarkdownRenders(
'A `code span with a footnote[^1] reference`.',
'<p>A <code>code span with a footnote[^1] reference</code>.</p>'
)

def test_footnote_reference_within_link(self):
"""Test footnote reference within a link."""

self.assertMarkdownRenders(
'A [link with a footnote[^1] reference](http://example.com).',
'<p>A <a href="http://example.com">link with a footnote[^1] reference</a>.</p>'
)

def test_footnote_reference_within_footnote_definition(self):
"""Test footnote definition containing another footnote reference."""

self.assertMarkdownRenders(
'Main footnote[^main].\n\n'
'[^main]: This footnote references another[^nested].\n'
'[^nested]: Nested footnote.',
'<p>Main footnote<sup id="fnref:main"><a class="footnote-ref" href="#fn:main">1</a></sup>.</p>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:main">\n'
'<p>This footnote references another<sup id="fnref:nested"><a class="footnote-ref" '
'href="#fn:nested">2</a></sup>.&#160;<a class="footnote-backref" href="#fnref:main" '
'title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'<li id="fn:nested">\n'
'<p>Nested footnote.&#160;<a class="footnote-backref" href="#fnref:nested" '
'title="Jump back to footnote 2 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>'
)

def test_footnote_reference_within_blockquote(self):
"""Test footnote reference within a blockquote."""

self.assertMarkdownRenders(
'> This is a quote with a footnote[^quote].\n\n[^quote]: Quote footnote.',
'<blockquote>\n'
'<p>This is a quote with a footnote<sup id="fnref:quote">'
'<a class="footnote-ref" href="#fn:quote">1</a></sup>.</p>\n'
'</blockquote>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:quote">\n'
'<p>Quote footnote.&#160;<a class="footnote-backref" href="#fnref:quote" '
'title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>'
)

def test_footnote_reference_within_list(self):
"""Test footnote reference within a list item."""

self.assertMarkdownRenders(
'1. First item with footnote[^note]\n1. Second item\n\n[^note]: List footnote.',
'<ol>\n'
'<li>First item with footnote<sup id="fnref:note">'
'<a class="footnote-ref" href="#fn:note">1</a></sup></li>\n'
'<li>Second item</li>\n'
'</ol>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:note">\n'
'<p>List footnote.&#160;<a class="footnote-backref" href="#fnref:note" '
'title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>'
)

def test_footnote_reference_within_html(self):
"""Test footnote reference within HTML tags."""

self.assertMarkdownRenders(
'A <span>footnote reference[^1] within a span element</span>.\n\n[^1]: The footnote.',
'<p>A <span>footnote reference<sup id="fnref:1">'
'<a class="footnote-ref" href="#fn:1">1</a>'
'</sup> within a span element</span>.</p>\n'
'<div class="footnote">\n'
'<hr />\n'
'<ol>\n'
'<li id="fn:1">\n'
'<p>The footnote.&#160;<a class="footnote-backref" href="#fnref:1" '
'title="Jump back to footnote 1 in the text">&#8617;</a></p>\n'
'</li>\n'
'</ol>\n'
'</div>'
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test parsing of footnote references within other structures.

@waylan waylan added the needs-review Needs to be reviewed and/or approved. label Jul 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-review Needs to be reviewed and/or approved.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Footnotes numbering
2 participants