@@ -302,3 +302,204 @@ def testCustomSubstitutions(self):
302302is the ‚mdash‘: \u2014
303303Must not be confused with ‚ndash‘ (\u2013 ) \u2026 ]</p>"""
304304 self .assertEqual (self .md .convert (text ), correct )
305+
306+
307+ class TestFootnotes (unittest .TestCase ):
308+ """Test Footnotes Extension."""
309+
310+ def setUp (self ):
311+ self .md = markdown .Markdown (extensions = ["footnotes" ])
312+
313+ def testBasicFootnote (self ):
314+ """ Test basic footnote syntax. """
315+ text = "This is a footnote reference[^1].\n \n [^1]: This is the footnote."
316+
317+ expected = (
318+ '<p>This is a footnote reference<sup id="fnref:1">'
319+ '<a class="footnote-ref" href="#fn:1">1</a></sup>.</p>\n '
320+ '<div class="footnote">\n '
321+ "<hr />\n "
322+ "<ol>\n "
323+ '<li id="fn:1">\n '
324+ "<p>This is the footnote. "
325+ '<a class="footnote-backref" href="#fnref:1" title="Jump back to '
326+ 'footnote 1 in the text">↩</a></p>\n '
327+ "</li>\n "
328+ "</ol>\n "
329+ "</div>"
330+ )
331+
332+ self .assertEqual (self .md .convert (text ), expected )
333+
334+ def testFootnoteOrder (self ):
335+ """ Test that footnotes are ordered correctly. """
336+ text = (
337+ "First footnote reference[^first]. Second footnote reference[^last].\n \n "
338+ "[^last]: Second footnote.\n [^first]: First footnote."
339+ )
340+
341+ expected = (
342+ '<p>First footnote reference<sup id="fnref:first"><a class="footnote-ref" '
343+ 'href="#fn:first">1</a></sup>. Second footnote reference<sup id="fnref:last">'
344+ '<a class="footnote-ref" href="#fn:last">2</a></sup>.</p>\n '
345+ '<div class="footnote">\n '
346+ "<hr />\n "
347+ "<ol>\n "
348+ '<li id="fn:first">\n '
349+ '<p>First footnote. <a class="footnote-backref" href="#fnref:first" '
350+ 'title="Jump back to footnote 1 in the text">↩</a></p>\n '
351+ "</li>\n "
352+ '<li id="fn:last">\n '
353+ '<p>Second footnote. <a class="footnote-backref" href="#fnref:last" '
354+ 'title="Jump back to footnote 2 in the text">↩</a></p>\n '
355+ "</li>\n "
356+ "</ol>\n "
357+ "</div>"
358+ )
359+
360+ self .assertEqual (self .md .convert (text ), expected )
361+
362+ def testFootnoteReferenceWithinCodeSpan (self ):
363+ """ Test footnote reference within a code span. """
364+
365+ text = "A `code span with a footnote[^1] reference`."
366+ expected = "<p>A <code>code span with a footnote[^1] reference</code>.</p>"
367+
368+ self .assertEqual (self .md .convert (text ), expected )
369+
370+ def testFootnoteReferenceInLink (self ):
371+ """ Test footnote reference within a link. """
372+
373+ text = "A [link with a footnote[^1] reference](http://example.com)."
374+ expected = '<p>A <a href="http://example.com">link with a footnote[^1] reference</a>.</p>'
375+
376+ self .assertEqual (self .md .convert (text ), expected )
377+
378+ def testDuplicateFootnoteReferences (self ):
379+ """ Test multiple references to the same footnote. """
380+ text = "First[^dup] and second[^dup] reference.\n \n [^dup]: Duplicate footnote."
381+
382+ expected = (
383+ '<p>First<sup id="fnref:dup">'
384+ '<a class="footnote-ref" href="#fn:dup">1</a></sup> and second<sup id="fnref2:dup">'
385+ '<a class="footnote-ref" href="#fn:dup">1</a></sup> reference.</p>\n '
386+ '<div class="footnote">\n '
387+ "<hr />\n "
388+ "<ol>\n "
389+ '<li id="fn:dup">\n '
390+ "<p>Duplicate footnote. "
391+ '<a class="footnote-backref" href="#fnref:dup" '
392+ 'title="Jump back to footnote 1 in the text">↩</a>'
393+ '<a class="footnote-backref" href="#fnref2:dup" '
394+ 'title="Jump back to footnote 1 in the text">↩</a></p>\n '
395+ "</li>\n "
396+ "</ol>\n "
397+ "</div>"
398+ )
399+
400+ self .assertEqual (self .md .convert (text ), expected )
401+
402+ def testFootnoteReferenceWithoutDefinition (self ):
403+ """ Test footnote reference without corresponding definition. """
404+ text = "This has a missing footnote[^missing]."
405+ expected = "<p>This has a missing footnote[^missing].</p>"
406+
407+ self .assertEqual (self .md .convert (text ), expected )
408+
409+ def testFootnoteDefinitionWithoutReference (self ):
410+ """ Test footnote definition without corresponding reference. """
411+ text = "No reference here.\n \n [^orphan]: Orphaned footnote."
412+
413+ self .assertIn ("fn:orphan" , self .md .convert (text ))
414+
415+ # For the opposite behavior:
416+ # self.assertNotIn("fn:orphan", self.md.convert(text))
417+
418+ def testMultilineFootnote (self ):
419+ """ Test footnote definition spanning multiple lines. """
420+
421+ text = (
422+ "Multi-line footnote[^multi].\n \n "
423+ "[^multi]: This is a footnote\n "
424+ " that spans multiple lines\n "
425+ " with proper indentation."
426+ )
427+
428+ expected = (
429+ '<p>Multi-line footnote<sup id="fnref:multi"><a class="footnote-ref" href="#fn:multi">1</a></sup>.</p>\n '
430+ '<div class="footnote">\n '
431+ '<hr />\n '
432+ '<ol>\n '
433+ '<li id="fn:multi">\n '
434+ '<p>This is a footnote\n '
435+ 'that spans multiple lines\n '
436+ 'with proper indentation. <a class="footnote-backref" href="#fnref:multi" '
437+ 'title="Jump back to footnote 1 in the text">↩</a></p>\n '
438+ '</li>\n '
439+ '</ol>\n '
440+ '</div>'
441+ )
442+ self .assertEqual (self .md .convert (text ), expected )
443+
444+ def testFootnoteInBlockquote (self ):
445+ """ Test footnote reference within a blockquote. """
446+ text = "> This is a quote with a footnote[^quote].\n \n [^quote]: Quote footnote."
447+
448+ result = self .md .convert (text )
449+ self .assertIn ("<blockquote>" , result )
450+ self .assertIn ("fnref:quote" , result )
451+
452+ def testFootnoteInList (self ):
453+ """ Test footnote reference within a list item. """
454+ text = "1. First item with footnote[^note]\n 1. Second item\n \n [^note]: List footnote."
455+
456+ result = self .md .convert (text )
457+ self .assertIn ("<ol>" , result )
458+ self .assertIn ("fnref:note" , result )
459+
460+ def testNestedFootnotes (self ):
461+ """ Test footnote definition containing another footnote reference. """
462+ text = (
463+ "Main footnote[^main].\n \n "
464+ "[^main]: This footnote references another[^nested].\n "
465+ "[^nested]: Nested footnote."
466+ )
467+ result = self .md .convert (text )
468+
469+ self .assertIn ("fnref:main" , result )
470+ self .assertIn ("fnref:nested" , result )
471+ self .assertIn ("fn:main" , result )
472+ self .assertIn ("fn:nested" , result )
473+
474+ def testFootnoteReset (self ):
475+ """ Test that footnotes are properly reset between documents. """
476+ text1 = "First doc[^1].\n \n [^1]: First footnote."
477+ text2 = "Second doc[^1].\n \n [^1]: Different footnote."
478+
479+ result1 = self .md .convert (text1 )
480+ self .md .reset ()
481+ result2 = self .md .convert (text2 )
482+
483+ self .assertIn ("First footnote" , result1 )
484+ self .assertIn ("Different footnote" , result2 )
485+ self .assertNotIn ("Different footnote" , result1 )
486+
487+ def testFootnoteIdWithSpecialChars (self ):
488+ """ Test footnote id containing special and unicode characters. """
489+ text = "Unicode footnote id[^!#¤%/()=?+}{§øé].\n \n [^!#¤%/()=?+}{§øé]: Footnote with unicode id."
490+
491+ self .assertIn ("fnref:!#¤%/()=?+}{§øé" , self .md .convert (text ))
492+
493+ def testFootnoteRefInHtml (self ):
494+ """ Test footnote reference within HTML tags. """
495+ text = "A <span>footnote reference[^1] in an HTML</span>.\n \n [^1]: The footnote."
496+
497+ self .assertIn ("fnref:1" , self .md .convert (text ))
498+
499+ def testFootnoteWithHtmlAndMarkdown (self ):
500+ """ Test footnote containing HTML and markdown elements. """
501+ text = "A footnote with style[^html].\n \n [^html]: Has *emphasis* and <strong>bold</strong>."
502+
503+ result = self .md .convert (text )
504+ self .assertIn ("<em>emphasis</em>" , result )
505+ self .assertIn ("<strong>bold</strong>" , result )
0 commit comments