@@ -302,3 +302,204 @@ def testCustomSubstitutions(self):
302
302
is the ‚mdash‘: \u2014
303
303
Must not be confused with ‚ndash‘ (\u2013 ) \u2026 ]</p>"""
304
304
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