From d7203c998ec86ff2855888f06e17a62bd725a1cd Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:34:00 +0200 Subject: [PATCH 01/16] Fix footnote ordering so footnotes are listed in the order in which their references appear Fixes #1367 --- markdown/extensions/footnotes.py | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index 30c081138..23b4c314c 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -33,6 +33,7 @@ FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX RE_REF_ID = re.compile(r'(fnref)(\d+)') +RE_REFERENCE = re.compile(r'(? None: """ Clear footnotes on reset, and prepare for distinct document. """ + self.footnote_order: list[str] = [] self.footnotes: OrderedDict[str, str] = OrderedDict() self.unique_prefix += 1 self.found_refs = {} @@ -150,6 +152,11 @@ def setFootnote(self, id: str, text: str) -> None: """ Store a footnote for later retrieval. """ self.footnotes[id] = text + def addFootnoteRef(self, id: str) -> None: + """ Store a footnote reference id in order of appearance. """ + if id not in self.footnote_order: + self.footnote_order.append(id) + def get_separator(self) -> str: """ Get the footnote separator. """ return self.getConfig("SEPARATOR") @@ -174,6 +181,8 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: if not list(self.footnotes.keys()): return None + self.reorderFootnoteDict() + div = etree.Element("div") div.set('class', 'footnote') etree.SubElement(div, "hr") @@ -212,9 +221,24 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: p.append(backlink) return div + def reorderFootnoteDict(self) -> None: + """ Reorder the footnotes dict based on the order of references found. """ + ordered_footnotes = OrderedDict() + + for ref in self.footnote_order: + if ref in self.footnotes: + ordered_footnotes[ref] = self.footnotes[ref] + + # Add back any footnotes that were defined but not referenced. + for id, text in self.footnotes.items(): + if id not in ordered_footnotes: + ordered_footnotes[id] = text + + self.footnotes = ordered_footnotes + class FootnoteBlockProcessor(BlockProcessor): - """ Find all footnote references and store for later use. """ + """ Find footnote definitions and references, storing both for later use. """ RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE) @@ -226,8 +250,14 @@ def test(self, parent: etree.Element, block: str) -> bool: return True def run(self, parent: etree.Element, blocks: list[str]) -> bool: - """ Find, set, and remove footnote definitions. """ + """ Find, set, and remove footnote definitions. Find footnote references.""" block = blocks.pop(0) + + # Find any footnote references in the block to determine order. + for match in RE_REFERENCE.finditer(block): + ref_id = match.group(1) + self.footnotes.addFootnoteRef(ref_id) + m = self.RE.search(block) if m: id = m.group(1) From 1d13ae4c308e1c6855d7cb19423789d326eba03b Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:38:42 +0200 Subject: [PATCH 02/16] Minor fixes for work on footnotes extension - Add changelog entry - Fix minor formatting issue --- docs/changelog.md | 6 ++++++ markdown/extensions/footnotes.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index d81e66ae3..51a7660fe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,12 @@ and this project adheres to the [Python Version Specification](https://packaging.python.org/en/latest/specifications/version-specifiers/). See the [Contributing Guide](contributing.md) for details. +## [unreleased] - Unreleased + +### Fixed + +* Fix issue with footnote ordering (#1367). + ## [3.8.2] - 2025-06-19 ### Fixed diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index 23b4c314c..a7bf39af4 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -224,11 +224,11 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: def reorderFootnoteDict(self) -> None: """ Reorder the footnotes dict based on the order of references found. """ ordered_footnotes = OrderedDict() - + for ref in self.footnote_order: if ref in self.footnotes: ordered_footnotes[ref] = self.footnotes[ref] - + # Add back any footnotes that were defined but not referenced. for id, text in self.footnotes.items(): if id not in ordered_footnotes: From a6d1e0f575ef325b2a934006d883f389f1afbdd4 Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:26:27 +0200 Subject: [PATCH 03/16] Add comprehensive tests for footnotes extension --- tests/test_extensions.py | 201 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index b8bc3c81c..b354ab35d 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -302,3 +302,204 @@ def testCustomSubstitutions(self): is the ‚mdash‘: \u2014 Must not be confused with ‚ndash‘ (\u2013) \u2026 ]
""" self.assertEqual(self.md.convert(text), correct) + + +class TestFootnotes(unittest.TestCase): + """Test Footnotes Extension.""" + + def setUp(self): + self.md = markdown.Markdown(extensions=["footnotes"]) + + def testBasicFootnote(self): + """ Test basic footnote syntax. """ + text = "This is a footnote reference[^1].\n\n[^1]: This is the footnote." + + expected = ( + 'This is a footnote reference' + '1.
\n' + 'This is the footnote. " + '↩
\n' + "First footnote reference1. Second footnote reference' + '2.
\n' + 'A code span with a footnote[^1] reference
.
A link with a footnote[^1] reference.
' + + self.assertEqual(self.md.convert(text), expected) + + def testDuplicateFootnoteReferences(self): + """ Test multiple references to the same footnote. """ + text = "First[^dup] and second[^dup] reference.\n\n[^dup]: Duplicate footnote." + + expected = ( + 'First' + '1 and second' + '1 reference.
\n' + '" + ) + + self.assertEqual(self.md.convert(text), expected) + + def testFootnoteReferenceWithoutDefinition(self): + """ Test footnote reference without corresponding definition. """ + text = "This has a missing footnote[^missing]." + expected = "This has a missing footnote[^missing].
" + + self.assertEqual(self.md.convert(text), expected) + + def testFootnoteDefinitionWithoutReference(self): + """ Test footnote definition without corresponding reference. """ + text = "No reference here.\n\n[^orphan]: Orphaned footnote." + + self.assertIn("fn:orphan", self.md.convert(text)) + + # For the opposite behavior: + # self.assertNotIn("fn:orphan", self.md.convert(text)) + + def testMultilineFootnote(self): + """ Test footnote definition spanning multiple lines. """ + + text = ( + "Multi-line footnote[^multi].\n\n" + "[^multi]: This is a footnote\n" + " that spans multiple lines\n" + " with proper indentation." + ) + + expected = ( + 'Multi-line footnote1.
\n' + 'This is a footnote\n' + 'that spans multiple lines\n' + 'with proper indentation. ↩
\n' + '", result) + self.assertIn("fnref:quote", result) + + def testFootnoteInList(self): + """ Test footnote reference within a list item. """ + text = "1. First item with footnote[^note]\n1. Second item\n\n[^note]: List footnote." + + result = self.md.convert(text) + self.assertIn("", result) + self.assertIn("fnref:note", result) + + def testNestedFootnotes(self): + """ Test footnote definition containing another footnote reference. """ + text = ( + "Main footnote[^main].\n\n" + "[^main]: This footnote references another[^nested].\n" + "[^nested]: Nested footnote." + ) + result = self.md.convert(text) + + self.assertIn("fnref:main", result) + self.assertIn("fnref:nested", result) + self.assertIn("fn:main", result) + self.assertIn("fn:nested", result) + + def testFootnoteReset(self): + """ Test that footnotes are properly reset between documents. """ + text1 = "First doc[^1].\n\n[^1]: First footnote." + text2 = "Second doc[^1].\n\n[^1]: Different footnote." + + result1 = self.md.convert(text1) + self.md.reset() + result2 = self.md.convert(text2) + + self.assertIn("First footnote", result1) + self.assertIn("Different footnote", result2) + self.assertNotIn("Different footnote", result1) + + def testFootnoteIdWithSpecialChars(self): + """ Test footnote id containing special and unicode characters. """ + text = "Unicode footnote id[^!#¤%/()=?+}{§øé].\n\n[^!#¤%/()=?+}{§øé]: Footnote with unicode id." + + self.assertIn("fnref:!#¤%/()=?+}{§øé", self.md.convert(text)) + + def testFootnoteRefInHtml(self): + """ Test footnote reference within HTML tags. """ + text = "A footnote reference[^1] in an HTML.\n\n[^1]: The footnote." + + self.assertIn("fnref:1", self.md.convert(text)) + + def testFootnoteWithHtmlAndMarkdown(self): + """ Test footnote containing HTML and markdown elements. """ + text = "A footnote with style[^html].\n\n[^html]: Has *emphasis* and bold." + + result = self.md.convert(text) + self.assertIn("emphasis", result) + self.assertIn("bold", result) From 0c2e4bf56734206c0bd592428df525a94da567df Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:23:45 +0200 Subject: [PATCH 04/16] Rework new tests for footnote extension - 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 --- tests/test_extensions.py | 201 ------------------ .../test_syntax/extensions/test_footnotes.py | 189 ++++++++++++++++ 2 files changed, 189 insertions(+), 201 deletions(-) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index b354ab35d..b8bc3c81c 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -302,204 +302,3 @@ def testCustomSubstitutions(self): is the ‚mdash‘: \u2014 Must not be confused with ‚ndash‘ (\u2013) \u2026 ]""" self.assertEqual(self.md.convert(text), correct) - - -class TestFootnotes(unittest.TestCase): - """Test Footnotes Extension.""" - - def setUp(self): - self.md = markdown.Markdown(extensions=["footnotes"]) - - def testBasicFootnote(self): - """ Test basic footnote syntax. """ - text = "This is a footnote reference[^1].\n\n[^1]: This is the footnote." - - expected = ( - '
This is a footnote reference' - '1.
\n' - '\n' - "" - ) - - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteOrder(self): - """ Test that footnotes are ordered correctly. """ - text = ( - "First footnote reference[^first]. Second footnote reference[^last].\n\n" - "[^last]: Second footnote.\n[^first]: First footnote." - ) - - expected = ( - '
\n" - "\n" - '
\n" - "- \n' - "
\n" - "This is the footnote. " - '↩
\n' - "First footnote reference1. Second footnote reference' - '2.
\n' - '\n' - "" - ) - - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteReferenceWithinCodeSpan(self): - """ Test footnote reference within a code span. """ - - text = "A `code span with a footnote[^1] reference`." - expected = "
\n" - "\n" - "A
" - - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteReferenceInLink(self): - """ Test footnote reference within a link. """ - - text = "A [link with a footnote[^1] reference](http://example.com)." - expected = 'code span with a footnote[^1] reference
.A link with a footnote[^1] reference.
' - - self.assertEqual(self.md.convert(text), expected) - - def testDuplicateFootnoteReferences(self): - """ Test multiple references to the same footnote. """ - text = "First[^dup] and second[^dup] reference.\n\n[^dup]: Duplicate footnote." - - expected = ( - 'First' - '1 and second' - '1 reference.
\n' - '" - ) - - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteReferenceWithoutDefinition(self): - """ Test footnote reference without corresponding definition. """ - text = "This has a missing footnote[^missing]." - expected = "This has a missing footnote[^missing].
" - - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteDefinitionWithoutReference(self): - """ Test footnote definition without corresponding reference. """ - text = "No reference here.\n\n[^orphan]: Orphaned footnote." - - self.assertIn("fn:orphan", self.md.convert(text)) - - # For the opposite behavior: - # self.assertNotIn("fn:orphan", self.md.convert(text)) - - def testMultilineFootnote(self): - """ Test footnote definition spanning multiple lines. """ - - text = ( - "Multi-line footnote[^multi].\n\n" - "[^multi]: This is a footnote\n" - " that spans multiple lines\n" - " with proper indentation." - ) - - expected = ( - 'Multi-line footnote1.
\n' - '\n' - '' - ) - self.assertEqual(self.md.convert(text), expected) - - def testFootnoteInBlockquote(self): - """ Test footnote reference within a blockquote. """ - text = "> This is a quote with a footnote[^quote].\n\n[^quote]: Quote footnote." - - result = self.md.convert(text) - self.assertIn("
\n' - '\n' - '
\n' - '- \n' - '
\n' - 'This is a footnote\n' - 'that spans multiple lines\n' - 'with proper indentation. ↩
\n' - '", result) - self.assertIn("fnref:quote", result) - - def testFootnoteInList(self): - """ Test footnote reference within a list item. """ - text = "1. First item with footnote[^note]\n1. Second item\n\n[^note]: List footnote." - - result = self.md.convert(text) - self.assertIn("", result) - self.assertIn("fnref:note", result) - - def testNestedFootnotes(self): - """ Test footnote definition containing another footnote reference. """ - text = ( - "Main footnote[^main].\n\n" - "[^main]: This footnote references another[^nested].\n" - "[^nested]: Nested footnote." - ) - result = self.md.convert(text) - - self.assertIn("fnref:main", result) - self.assertIn("fnref:nested", result) - self.assertIn("fn:main", result) - self.assertIn("fn:nested", result) - - def testFootnoteReset(self): - """ Test that footnotes are properly reset between documents. """ - text1 = "First doc[^1].\n\n[^1]: First footnote." - text2 = "Second doc[^1].\n\n[^1]: Different footnote." - - result1 = self.md.convert(text1) - self.md.reset() - result2 = self.md.convert(text2) - - self.assertIn("First footnote", result1) - self.assertIn("Different footnote", result2) - self.assertNotIn("Different footnote", result1) - - def testFootnoteIdWithSpecialChars(self): - """ Test footnote id containing special and unicode characters. """ - text = "Unicode footnote id[^!#¤%/()=?+}{§øé].\n\n[^!#¤%/()=?+}{§øé]: Footnote with unicode id." - - self.assertIn("fnref:!#¤%/()=?+}{§øé", self.md.convert(text)) - - def testFootnoteRefInHtml(self): - """ Test footnote reference within HTML tags. """ - text = "A footnote reference[^1] in an HTML.\n\n[^1]: The footnote." - - self.assertIn("fnref:1", self.md.convert(text)) - - def testFootnoteWithHtmlAndMarkdown(self): - """ Test footnote containing HTML and markdown elements. """ - text = "A footnote with style[^html].\n\n[^html]: Has *emphasis* and bold." - - result = self.md.convert(text) - self.assertIn("emphasis", result) - self.assertIn("bold", result) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 6f504e39c..01659b3f9 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -336,3 +336,192 @@ def test_superscript_text(self): '', extension_configs={'footnotes': {'SUPERSCRIPT_TEXT': '[{}]'}} ) + + 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.', + '
\n' - '\n' + '' ) def test_footnote_reference_within_html(self): From bc3f49325da55ae4bdfcfb671bc3c061edee413a Mon Sep 17 00:00:00 2001 From: Waylan LimbergFirst footnote reference1. Second footnote reference' + '2.
\n' + '\n' + '' + ) + + 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`.', + '
\n' + '\n' + 'A
' + ) + + 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).', + 'code span with a footnote[^1] reference
.A link with a footnote[^1] reference.
' + ) + + 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.', + 'Main footnote1.
\n' + '\n' + '' + ) + + 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.', + '
\n' + '\n' + '\n' + '\n' + 'This is a quote with a footnote' + '1.
\n' + '\n' + '' + ) + + 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.', + '
\n' + '\n' + '
\n' + '- \n' + '
\n' + 'Quote footnote. ↩
\n' + '\n' + '
\n' + '- First item with footnote' + '1
\n' + '- Second item
\n' + '\n' + '' + ) + + def test_footnote_reference_within_html(self): + """Test footnote reference within HTML tags.""" + + self.assertMarkdownRenders( + 'A footnote reference[^1] within a span element.\n\n[^1]: The footnote.', + '
\n' + '\n' + '
\n' + '- \n' + '
\n' + 'List footnote. ↩
\n' + 'A footnote reference' + '1' + ' within a span element.
\n' + '\n' + '' + ) + + def test_duplicate_footnote_references(self): + """Test multiple references to the same footnote.""" + + self.assertMarkdownRenders( + 'First[^dup] and second[^dup] reference.\n\n[^dup]: Duplicate footnote.', + '
\n' + '\n' + '
\n' + '- \n' + '
\n' + 'The footnote. ↩
\n' + 'First' + '1 and second' + '1 reference.
\n' + '' + ) + + def test_footnote_reference_without_definition(self): + """Test footnote reference without corresponding definition.""" + + self.assertMarkdownRenders( + 'This has a missing footnote[^missing].', + 'This has a missing footnote[^missing].
' + ) + + def test_footnote_definition_without_reference(self): + """Test footnote definition without corresponding reference.""" + + self.assertMarkdownRenders( + 'No reference here.\n\n[^orphan]: Orphaned footnote.', + 'No reference here.
\n' + '\n' + '' + ) + + def test_footnote_id_with_special_chars(self): + """Test footnote id containing special and Unicode characters.""" + + self.assertMarkdownRenders( + 'Special footnote id[^!#¤%/()=?+}{§øé].\n\n[^!#¤%/()=?+}{§øé]: The footnote.', + '
\n' + '\n' + '
\n' + '- \n' + '
\n' + 'Orphaned footnote. ↩
\n' + 'Special footnote id' + '1.
\n' + '\n' + '' + ) From ce45459432b06aa63c2df7eab350957f1910742d Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Sun, 27 Jul 2025 09:54:27 +0200 Subject: [PATCH 05/16] Add tricky edge case that messes with the reordering introduced in d7203c9 --- .../test_syntax/extensions/test_footnotes.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 01659b3f9..fbda36bef 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -361,6 +361,34 @@ def test_footnote_order(self): '' ) + def test_footnote_order_tricky(self): + """Test that tricky sequence of footnote references.""" + + self.assertMarkdownRenders( + '`Footnote reference in code spans should be ignored[^tricky]`. ' + 'A footnote reference[^ordinary]. ' + 'Another footnote reference[^tricky].\n\n' + '[^ordinary]: This should be the first footnote.\n' + '[^tricky]: This should be the second footnote.', + '
\n' + '\n' + '
\n' + '- \n' + '
\n' + 'The footnote. ↩
\n' + '\n' + '
Footnote reference in code spans should be ignored[^tricky]
. ' + 'A footnote reference1. Another footnote reference' + '2.\n' + '' + ) + def test_footnote_reference_within_code_span(self): """Test footnote reference within a code span.""" From 5d7f8ca75789dc16ef234f007b520b66af770d08 Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:27:38 +0200 Subject: [PATCH 06/16] Refactor reordering to depend on inline processing - Remove parts of the previous implementation - Add FootnoteReorderingProcessor which reorders the footnotes if necessary - Move backlink title compatability trick to main class init method to avoid repetition --- markdown/extensions/footnotes.py | 77 +++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index a7bf39af4..0d08dc717 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -72,6 +72,9 @@ def __init__(self, **kwargs): self.found_refs: dict[str, int] = {} self.used_refs: set[str] = set() + # Backward compatibility with old '%d' placeholder + self.setConfig('BACKLINK_TITLE', self.getConfig("BACKLINK_TITLE").replace("%d", "{}")) + self.reset() def extendMarkdown(self, md): @@ -90,6 +93,11 @@ def extendMarkdown(self, md): # `codehilite`) so they can run on the the contents of the div. md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50) + # Insert a tree-processor to reorder the footnotes if necessary. This must be after + # `inline` tree-processor so it can access the footnote reference order + # (self.footnote_order) that gets populated by the FootnoteInlineProcessor. + md.treeprocessors.register(FootnoteReorderingProcessor(self), 'footnote-reorder', 19) + # Insert a tree-processor that will run after inline is done. # In this tree-processor we want to check our duplicate footnote tracker # And add additional `backrefs` to the footnote pointing back to the @@ -181,17 +189,12 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: if not list(self.footnotes.keys()): return None - self.reorderFootnoteDict() - div = etree.Element("div") div.set('class', 'footnote') etree.SubElement(div, "hr") ol = etree.SubElement(div, "ol") surrogate_parent = etree.Element("div") - # Backward compatibility with old '%d' placeholder - backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}") - for index, id in enumerate(self.footnotes.keys(), start=1): li = etree.SubElement(ol, "li") li.set("id", self.makeFootnoteId(id)) @@ -207,7 +210,7 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: backlink.set("class", "footnote-backref") backlink.set( "title", - backlink_title.format(index) + self.getConfig('BACKLINK_TITLE').format(index) ) backlink.text = FN_BACKLINK_TEXT @@ -221,21 +224,6 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: p.append(backlink) return div - def reorderFootnoteDict(self) -> None: - """ Reorder the footnotes dict based on the order of references found. """ - ordered_footnotes = OrderedDict() - - for ref in self.footnote_order: - if ref in self.footnotes: - ordered_footnotes[ref] = self.footnotes[ref] - - # Add back any footnotes that were defined but not referenced. - for id, text in self.footnotes.items(): - if id not in ordered_footnotes: - ordered_footnotes[id] = text - - self.footnotes = ordered_footnotes - class FootnoteBlockProcessor(BlockProcessor): """ Find footnote definitions and references, storing both for later use. """ @@ -253,11 +241,6 @@ def run(self, parent: etree.Element, blocks: list[str]) -> bool: """ Find, set, and remove footnote definitions. Find footnote references.""" block = blocks.pop(0) - # Find any footnote references in the block to determine order. - for match in RE_REFERENCE.finditer(block): - ref_id = match.group(1) - self.footnotes.addFootnoteRef(ref_id) - m = self.RE.search(block) if m: id = m.group(1) @@ -342,13 +325,15 @@ def __init__(self, pattern: str, footnotes: FootnoteExtension): def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]: id = m.group(1) if id in self.footnotes.footnotes.keys(): + self.footnotes.addFootnoteRef(id) + sup = etree.Element("sup") a = etree.SubElement(sup, "a") sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True)) a.set('href', '#' + self.footnotes.makeFootnoteId(id)) a.set('class', 'footnote-ref') a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format( - list(self.footnotes.footnotes.keys()).index(id) + 1 + self.footnotes.footnote_order.index(id) + 1 ) return sup, m.start(0), m.end(0) else: @@ -431,6 +416,44 @@ def run(self, root: etree.Element) -> None: root.append(footnotesDiv) +class FootnoteReorderingProcessor(Treeprocessor): + """ Reorder list items in the footnotes div. """ + + def __init__(self, footnotes: FootnoteExtension): + self.footnotes = footnotes + + def run(self, root: etree.Element) -> None: + if not self.footnotes.footnotes: + return + 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 reorder_footnotes(self, parent: etree.Element) -> None: + old_list = parent.find('ol') + parent.remove(old_list) + items = old_list.findall('li') + + def order_by_id(li) -> int: + id = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)[-1] + return ( + self.footnotes.footnote_order.index(id) + if id in self.footnotes.footnote_order + else len(self.footnotes.footnotes) + ) + + items = sorted(items, key=order_by_id) + + new_list = etree.SubElement(parent, 'ol') + + for index, item in enumerate(items, start=1): + backlink = item.find('.//a[@class="footnote-backref"]') + backlink.set("title", self.footnotes.getConfig("BACKLINK_TITLE").format(index)) + new_list.append(item) + + class FootnotePostprocessor(Postprocessor): """ Replace placeholders with html entities. """ def __init__(self, footnotes: FootnoteExtension): From 1c160ef29b77c3354a0dd80fa7181343cc2577a9 Mon Sep 17 00:00:00 2001 From: Waylan Limberg
\n' + '\n' + 'Date: Wed, 30 Jul 2025 09:54:46 -0400 Subject: [PATCH 07/16] Ensure inlinepatterns iterate through elements in document order --- markdown/treeprocessors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markdown/treeprocessors.py b/markdown/treeprocessors.py index 83630999e..9a27446d4 100644 --- a/markdown/treeprocessors.py +++ b/markdown/treeprocessors.py @@ -368,7 +368,7 @@ def run(self, tree: etree.Element, ancestors: list[str] | None = None) -> etree. stack = [(tree, tree_parents)] while stack: - currElement, parents = stack.pop() + currElement, parents = stack.pop(0) self.ancestors = parents self.__build_ancestors(currElement, self.ancestors) From 62b12b73133c906a137ef53d19c27feb0d575b0f Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Wed, 30 Jul 2025 10:32:51 -0400 Subject: [PATCH 08/16] Add loose list test --- .../test_syntax/extensions/test_footnotes.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index fbda36bef..673ada914 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -470,6 +470,45 @@ def test_footnote_reference_within_list(self): '' ) + def test_footnote_references_within_loose_list(self): + """Test footnote reference within a list item.""" + + self.assertMarkdownRenders( + self.dedent( + ''' + * Reference to [^first] + + * Reference to [^second] + + [^first]: First footnote definition + [^second]: Second footnote definition + ''' + ), + self.dedent( + ''' + + + ''' + ) + ) + def test_footnote_reference_within_html(self): """Test footnote reference within HTML tags.""" From 5e437a0bbac6860a0ccf7ea435db41781a25fd1a Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Wed, 30 Jul 2025 10:44:25 -0400 Subject: [PATCH 09/16] lint cleanup --- markdown/extensions/footnotes.py | 2 +- .../test_syntax/extensions/test_footnotes.py | 44 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index 0d08dc717..c2a10d674 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -95,7 +95,7 @@ def extendMarkdown(self, md): # Insert a tree-processor to reorder the footnotes if necessary. This must be after # `inline` tree-processor so it can access the footnote reference order - # (self.footnote_order) that gets populated by the FootnoteInlineProcessor. + # (`self.footnote_order`) that gets populated by the `FootnoteInlineProcessor`. md.treeprocessors.register(FootnoteReorderingProcessor(self), 'footnote-reorder', 19) # Insert a tree-processor that will run after inline is done. diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 673ada914..766cfb65a 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -484,29 +484,27 @@ def test_footnote_references_within_loose_list(self): [^second]: Second footnote definition ''' ), - self.dedent( - ''' - - - ''' - ) + '\n' + ' \n' + '\n' ) def test_footnote_reference_within_html(self): From da1e20d35d298f800d5b91ae224b4162e69f139b Mon Sep 17 00:00:00 2001 From: Waylan Limberg
\n' + '\n' + 'Date: Wed, 30 Jul 2025 10:48:33 -0400 Subject: [PATCH 10/16] fix my cleanup --- tests/test_syntax/extensions/test_footnotes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 766cfb65a..ff1c28560 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -504,7 +504,7 @@ def test_footnote_references_within_loose_list(self): 'title="Jump back to footnote 2 in the text">↩\n' '\n' ' Date: Wed, 30 Jul 2025 11:17:45 -0400 Subject: [PATCH 11/16] cleanup comment --- tests/test_syntax/extensions/test_footnotes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index ff1c28560..fd9fa2493 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -471,7 +471,7 @@ def test_footnote_reference_within_list(self): ) def test_footnote_references_within_loose_list(self): - """Test footnote reference within a list item.""" + """Test footnote references within loose list items.""" self.assertMarkdownRenders( self.dedent( From d8ecae6e4d930f9e7362e8a82df7c4c02886cf6d Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:44:12 +0200 Subject: [PATCH 12/16] Add the requested changes: - Fix docstrings in footnotes extension - Modify style of new tests to use self.dedent() - Add an entry in the changelog concerning the change to ensure inline processing occurs in document order --- docs/changelog.md | 1 + markdown/extensions/footnotes.py | 4 +- .../test_syntax/extensions/test_footnotes.py | 94 +++++++++++++++---- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index b819f8710..84ddde9de 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,7 @@ See the [Contributing Guide](contributing.md) for details. * Fix handling of incomplete HTML tags in code spans in Python 3.14. * Fix issue with footnote ordering (#1367). +* Ensure inline processing iterates through elements in document order. ## [3.8.2] - 2025-06-19 diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index c2a10d674..b76859a9c 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -226,7 +226,7 @@ def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None: class FootnoteBlockProcessor(BlockProcessor): - """ Find footnote definitions and references, storing both for later use. """ + """ Find footnote definitions and store for later use. """ RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE) @@ -238,7 +238,7 @@ def test(self, parent: etree.Element, block: str) -> bool: return True def run(self, parent: etree.Element, blocks: list[str]) -> bool: - """ Find, set, and remove footnote definitions. Find footnote references.""" + """ Find, set, and remove footnote definitions. """ block = blocks.pop(0) m = self.RE.search(block) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index fd9fa2493..086aaff85 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -341,8 +341,14 @@ 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.', + self.dedent( + """ + First footnote reference[^first]. Second footnote reference[^last]. + + [^last]: Second footnote. + [^first]: First footnote. + """ + ), ' First footnote reference1. Second footnote reference' '2.
\n' @@ -362,17 +368,23 @@ def test_footnote_order(self): ) def test_footnote_order_tricky(self): - """Test that tricky sequence of footnote references.""" + """Test a tricky sequence of footnote references.""" self.assertMarkdownRenders( - '`Footnote reference in code spans should be ignored[^tricky]`. ' - 'A footnote reference[^ordinary]. ' - 'Another footnote reference[^tricky].\n\n' - '[^ordinary]: This should be the first footnote.\n' - '[^tricky]: This should be the second footnote.', - '
Footnote reference in code spans should be ignored[^tricky]
. ' - 'A footnote reference1. Another footnote reference' + self.dedent( + """ + `Footnote reference in code spans should be ignored[^tricky]`. + A footnote reference[^ordinary]. + Another footnote reference[^tricky]. + + [^ordinary]: This should be the first footnote. + [^tricky]: This should be the second footnote. + """ + ), + '\n' '
Footnote reference in code spans should be ignored[^tricky]
.\n' + 'A footnote reference' + '1.\n' + 'Another footnote reference' '2.\n' '
\n' @@ -409,9 +421,14 @@ 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.', + self.dedent( + """ + Main footnote[^main]. + + [^main]: This footnote references another[^nested]. + [^nested]: Nested footnote. + """ + ), 'Main footnote1.
\n' '\n' '
\n' @@ -433,7 +450,13 @@ 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.', + self.dedent( + """ + > This is a quote with a footnote[^quote]. + + [^quote]: Quote footnote. + """ + ), '\n' 'This is a quote with a footnote' '1.
\n' @@ -453,7 +476,14 @@ 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.', + self.dedent( + """ + 1. First item with footnote[^note] + 1. Second item + + [^note]: List footnote. + """ + ), '\n' '
- First item with footnote' '1
\n' @@ -511,7 +541,13 @@ def test_footnote_reference_within_html(self): """Test footnote reference within HTML tags.""" self.assertMarkdownRenders( - 'A footnote reference[^1] within a span element.\n\n[^1]: The footnote.', + self.dedent( + """ + A footnote reference[^1] within a span element. + + [^1]: The footnote. + """ + ), 'A footnote reference' '1' ' within a span element.
\n' @@ -530,7 +566,13 @@ def test_duplicate_footnote_references(self): """Test multiple references to the same footnote.""" self.assertMarkdownRenders( - 'First[^dup] and second[^dup] reference.\n\n[^dup]: Duplicate footnote.', + self.dedent( + """ + First[^dup] and second[^dup] reference. + + [^dup]: Duplicate footnote. + """ + ), 'First' '1 and second' '1 reference.
\n' @@ -560,7 +602,13 @@ def test_footnote_definition_without_reference(self): """Test footnote definition without corresponding reference.""" self.assertMarkdownRenders( - 'No reference here.\n\n[^orphan]: Orphaned footnote.', + self.dedent( + """ + No reference here. + + [^orphan]: Orphaned footnote. + """ + ), 'No reference here.
\n' '\n' '
\n' @@ -577,7 +625,13 @@ def test_footnote_id_with_special_chars(self): """Test footnote id containing special and Unicode characters.""" self.assertMarkdownRenders( - 'Special footnote id[^!#¤%/()=?+}{§øé].\n\n[^!#¤%/()=?+}{§øé]: The footnote.', + self.dedent( + """ + Special footnote id[^!#¤%/()=?+}{§øé]. + + [^!#¤%/()=?+}{§øé]: The footnote. + """ + ), 'Special footnote id' '1.
\n' '\n' From ed7ff42201e294d6a678408a6cf9b98d28709c17 Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:05:28 +0200 Subject: [PATCH 13/16] Add config option to support previous behavior - Don't register FootnoteReorderingProcessor if USE_DEFINITION_ORDER - Footnote reference numbering follows config - Test added - Config option added to docs with note on behavior change in v3.9.0 - Update changelog --- docs/changelog.md | 9 ++++-- docs/extensions/footnotes.md | 9 ++++++ markdown/extensions/footnotes.py | 17 +++++++--- .../test_syntax/extensions/test_footnotes.py | 31 +++++++++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 84ddde9de..47239577f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,11 +12,16 @@ See the [Contributing Guide](contributing.md) for details. ## [Unreleased] +### Changed + +* Footnotes are now ordered by the occurrence of their references in the document. + A new config option for `footnotes`, `USE_DEFINITION_ORDER`, has been added to support + restoring the previous behavior of ordering footnotes by the occurrence of definitions. + ### Fixed -* Fix handling of incomplete HTML tags in code spans in Python 3.14. -* Fix issue with footnote ordering (#1367). * Ensure inline processing iterates through elements in document order. +* Fix handling of incomplete HTML tags in code spans in Python 3.14. ## [3.8.2] - 2025-06-19 diff --git a/docs/extensions/footnotes.md b/docs/extensions/footnotes.md index e841a324d..6dd0323a7 100644 --- a/docs/extensions/footnotes.md +++ b/docs/extensions/footnotes.md @@ -98,6 +98,15 @@ The following options are provided to configure the output: * **`SEPARATOR`**: The text string used to set the footnote separator. Defaults to `:`. +* **`USE_DEFINITION_ORDER`**: + Whether to order footnotes by the occurence of footnote definitions + in the document. Defaults to `False`. + + Introduced in version 3.9.0, this option allows footnotes to be ordered + by the occurence of their definitions in the document, rather than by the + order of their references in the text. This was the behavior of + previous versions of the extension. + A trivial example: ```python diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index b76859a9c..13ecf7c22 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -62,6 +62,9 @@ def __init__(self, **kwargs): ], 'SEPARATOR': [ ':', 'Footnote separator.' + ], + 'USE_DEFINITION_ORDER': [ + False, 'Whether to order footnotes by footnote content rather than by footnote label.' ] } """ Default configuration options. """ @@ -96,7 +99,8 @@ def extendMarkdown(self, md): # Insert a tree-processor to reorder the footnotes if necessary. This must be after # `inline` tree-processor so it can access the footnote reference order # (`self.footnote_order`) that gets populated by the `FootnoteInlineProcessor`. - md.treeprocessors.register(FootnoteReorderingProcessor(self), 'footnote-reorder', 19) + if not self.getConfig("USE_DEFINITION_ORDER"): + md.treeprocessors.register(FootnoteReorderingProcessor(self), 'footnote-reorder', 19) # Insert a tree-processor that will run after inline is done. # In this tree-processor we want to check our duplicate footnote tracker @@ -327,14 +331,19 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None if id in self.footnotes.footnotes.keys(): self.footnotes.addFootnoteRef(id) + if not self.footnotes.getConfig("USE_DEFINITION_ORDER"): + # Order by reference + footnote_num = self.footnotes.footnote_order.index(id) + 1 + else: + # Order by definition + footnote_num = list(self.footnotes.footnotes.keys()).index(id) + 1 + sup = etree.Element("sup") a = etree.SubElement(sup, "a") sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True)) a.set('href', '#' + self.footnotes.makeFootnoteId(id)) a.set('class', 'footnote-ref') - a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format( - self.footnotes.footnote_order.index(id) + 1 - ) + a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format(footnote_num) return sup, m.start(0), m.end(0) else: return None, None, None diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 086aaff85..b4bbdc92b 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -401,6 +401,37 @@ def test_footnote_order_tricky(self): '' ) + def test_footnote_order_by_definition(self): + """Test that footnotes occur in order of definition occurence when so configured.""" + + self.assertMarkdownRenders( + self.dedent( + """ + First footnote reference[^last_def]. Second footnote reference[^first_def]. + + [^first_def]: First footnote. + [^last_def]: Second footnote. + """ + ), + 'First footnote reference2. Second footnote reference' + '1.
\n' + '\n' + '', + extension_configs={'footnotes': {'USE_DEFINITION_ORDER': True}} + ) + def test_footnote_reference_within_code_span(self): """Test footnote reference within a code span.""" From 345f52eed40315299e33eac81972e728f888f68c Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:10:49 +0200 Subject: [PATCH 14/16] Update docs to clarify terminology --- docs/extensions/footnotes.md | 42 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/extensions/footnotes.md b/docs/extensions/footnotes.md index 6dd0323a7..0366e858b 100644 --- a/docs/extensions/footnotes.md +++ b/docs/extensions/footnotes.md @@ -24,26 +24,32 @@ the output. Example: ```md -Footnotes[^1] have a label[^@#$%] and the footnote's content. +Footnotes have a name, a reference[^1], and a definition[^@#$%]. -[^1]: This is a footnote content. -[^@#$%]: A footnote on the label: "@#$%". +[^1]: This is a footnote definition. +[^@#$%]: A footnote with the name "@#$%". ``` -A footnote label must start with a caret `^` and may contain any inline text -(including spaces) between a set of square brackets `[]`. Only the first -caret has any special meaning. - -A footnote content must start with the label followed by a colon and at least -one space. The label used to define the content must exactly match the label used -in the body (including capitalization and white space). The content would then -follow the label either on the same line or on the next line. The content may -contain multiple lines, paragraphs, code blocks, blockquotes and most any other -markdown syntax. The additional lines must be indented one level (four spaces or -one tab). - -When working with multiple blocks, it may be helpful to start the content on a -separate line from the label which defines the content. This way the entire block +A **footnote name** is a string that uniquely identifies a footnote within the +document. It may contain any character which is valid for an HTML id attribute +(including spaces). Examples: `1` in `[^1]` and `@#$%` in `[^@#$%]`. + +A **footnote reference** is a link within the text body to a footnote definition. +A footnote reference contains the footnote name prefixed by a caret `^` and enclosed +in square brackets `[]`. Examples: `[^1]` and `[^@#$%]`. In the output, footnote +references are replaced by a superscript number that links to the footnote definition. + +A **footnote definition** must start with the corresponding footnote reference +followed by a colon and at least one space. The reference must exactly match +the reference used in the body (including capitalization and white space). +The content of the definition would then follow either on the same line +(`[^1]: This is a footnote definition.`) or on the next line. +Footnote definitions may contain multiple lines, paragraphs, code blocks, +blockquotes and most any other markdown syntax. The additional lines must be +indented one level (four spaces or one tab). + +When working with multiple blocks, it may be helpful to start the definition on a +separate line from the reference which defines the content. This way the entire block is indented consistently and any errors are more easily discernible by the author. ```md @@ -118,7 +124,7 @@ Resetting Instance State Footnote definitions are stored within the `markdown.Markdown` class instance between multiple runs of the class. This allows footnotes from all runs to be included in -output, with links and references that are unique, even though the class has been +output, with links and references that are unique, even though the class has been called multiple times. However, if needed, the definitions can be cleared between runs by calling `reset`. From a0755e84c5c45ebef1a3c35f48b10404576af647 Mon Sep 17 00:00:00 2001 From: Anders Eskildsen <22001464+aeskildsen@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:32:31 +0200 Subject: [PATCH 15/16] Add 'word' as an example of a footnote name/id --- docs/extensions/footnotes.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/extensions/footnotes.md b/docs/extensions/footnotes.md index 0366e858b..4b1332490 100644 --- a/docs/extensions/footnotes.md +++ b/docs/extensions/footnotes.md @@ -24,15 +24,16 @@ the output. Example: ```md -Footnotes have a name, a reference[^1], and a definition[^@#$%]. +Footnotes have a name, a reference[^1], and a definition[^word]. [^1]: This is a footnote definition. -[^@#$%]: A footnote with the name "@#$%". +[^word]: A footnote with the name "word". ``` -A **footnote name** is a string that uniquely identifies a footnote within the +A **footnote name** is a string that uniquely identifies a footnote within the document. It may contain any character which is valid for an HTML id attribute -(including spaces). Examples: `1` in `[^1]` and `@#$%` in `[^@#$%]`. +(including spaces). Examples: `1` in `[^1]`, `word` in `[^word]`, +and `@#$%` in `[^@#$%]`. A **footnote reference** is a link within the text body to a footnote definition. A footnote reference contains the footnote name prefixed by a caret `^` and enclosed From dbd70683e962d8670d1dd683c69ae1e1e4884e08 Mon Sep 17 00:00:00 2001 From: Waylan Limberg
\n' + '\n' + 'Date: Fri, 1 Aug 2025 14:25:10 -0400 Subject: [PATCH 16/16] fix spelling --- docs/changelog.md | 7 ++++--- docs/extensions/footnotes.md | 4 ++-- tests/test_syntax/extensions/test_footnotes.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 47239577f..7afa81bb2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,9 +14,10 @@ See the [Contributing Guide](contributing.md) for details. ### Changed -* Footnotes are now ordered by the occurrence of their references in the document. - A new config option for `footnotes`, `USE_DEFINITION_ORDER`, has been added to support - restoring the previous behavior of ordering footnotes by the occurrence of definitions. +* Footnotes are now ordered by the occurrence of their references in the + document. A new configuration option for the footnotes extension, + `USE_DEFINITION_ORDER`, has been added to support restoring the previous + behavior of ordering footnotes by the occurrence of definitions. ### Fixed diff --git a/docs/extensions/footnotes.md b/docs/extensions/footnotes.md index 4b1332490..7d033478f 100644 --- a/docs/extensions/footnotes.md +++ b/docs/extensions/footnotes.md @@ -106,11 +106,11 @@ The following options are provided to configure the output: The text string used to set the footnote separator. Defaults to `:`. * **`USE_DEFINITION_ORDER`**: - Whether to order footnotes by the occurence of footnote definitions + Whether to order footnotes by the occurrence of footnote definitions in the document. Defaults to `False`. Introduced in version 3.9.0, this option allows footnotes to be ordered - by the occurence of their definitions in the document, rather than by the + by the occurrence of their definitions in the document, rather than by the order of their references in the text. This was the behavior of previous versions of the extension. diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index b4bbdc92b..070fa27fc 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -402,7 +402,7 @@ def test_footnote_order_tricky(self): ) def test_footnote_order_by_definition(self): - """Test that footnotes occur in order of definition occurence when so configured.""" + """Test that footnotes occur in order of definition occurrence when so configured.""" self.assertMarkdownRenders( self.dedent(