From 895e46751b2d2cb7a9be14b308505ccec51cf9fe Mon Sep 17 00:00:00 2001
From: mscherer
' . $backlinks . '
' . "\n"; } $html .= '' . "\n"; @@ -964,6 +971,32 @@ protected function renderFootnotesSection(): string return $html; } + /** + * Generate backlink(s) for a footnote + * + * @param int $number Footnote number + * @param int $refCount Number of times footnote was referenced + */ + protected function generateBacklinks(int $number, int $refCount): string + { + if ($refCount <= 1) { + // Single reference - simple backlink + return '↩︎'; + } + + // Multiple references - generate numbered backlinks + $links = []; + for ($i = 1; $i <= $refCount; $i++) { + $refId = 'fnref' . $number; + if ($i > 1) { + $refId .= '-' . $i; + } + $links[] = '↩︎' . $i . ''; + } + + return implode(' ', $links); + } + protected function renderFootnoteRef(FootnoteRef $node): string { $label = $node->getLabel(); @@ -975,8 +1008,21 @@ protected function renderFootnoteRef(FootnoteRef $node): string } $number = $this->footnoteNumbers[$label]; + // Track reference count for this footnote to generate unique IDs + if (!isset($this->footnoteRefCounts[$label])) { + $this->footnoteRefCounts[$label] = 0; + } + $this->footnoteRefCounts[$label]++; + $refCount = $this->footnoteRefCounts[$label]; + + // Generate unique ID: fnref1 for first, fnref1-2 for second, etc. + $refId = 'fnref' . $number; + if ($refCount > 1) { + $refId .= '-' . $refCount; + } + // Format: 1 - return '' . $number . ''; + return '' . $number . ''; } protected function renderMath(Math $node): string diff --git a/tests/TestCase/DjotConverterTest.php b/tests/TestCase/DjotConverterTest.php index e534674..558e97d 100644 --- a/tests/TestCase/DjotConverterTest.php +++ b/tests/TestCase/DjotConverterTest.php @@ -2380,4 +2380,60 @@ public function testBackticksWithClosingFenceIsCodeBlock(): void $this->assertStringContainsString('', $result);
$this->assertStringContainsString('x = y + 3', $result);
}
+
+ /**
+ * Test that multiple references to the same footnote get unique IDs
+ *
+ * Fix for GitHub issue jgm/djot#348: Multiple calls to the same note
+ * should generate unique HTML-compliant IDs with suffixes.
+ */
+ public function testMultipleFootnoteReferencesGetUniqueIds(): void
+ {
+ $djot = <<<'DJOT'
+First ref[^note].
+
+Second ref[^note].
+
+Third ref[^note].
+
+[^note]: The footnote.
+DJOT;
+
+ $result = $this->converter->convert($djot);
+
+ // Each reference should have a unique ID
+ $this->assertStringContainsString('id="fnref1"', $result);
+ $this->assertStringContainsString('id="fnref1-2"', $result);
+ $this->assertStringContainsString('id="fnref1-3"', $result);
+
+ // All should link to the same footnote
+ $this->assertSame(3, substr_count($result, 'href="#fn1"'));
+
+ // Footnote should have multiple backlinks
+ $this->assertStringContainsString('href="#fnref1"', $result);
+ $this->assertStringContainsString('href="#fnref1-2"', $result);
+ $this->assertStringContainsString('href="#fnref1-3"', $result);
+
+ // Single footnote (no duplicates)
+ $this->assertSame(1, substr_count($result, 'id="fn1"'));
+ }
+
+ public function testSingleFootnoteReferenceNoSuffix(): void
+ {
+ $djot = <<<'DJOT'
+Single ref[^note].
+
+[^note]: The footnote.
+DJOT;
+
+ $result = $this->converter->convert($djot);
+
+ // Single reference - no suffix needed
+ $this->assertStringContainsString('id="fnref1"', $result);
+ $this->assertStringNotContainsString('fnref1-', $result);
+
+ // Simple backlink without numbering
+ $this->assertStringContainsString('href="#fnref1"', $result);
+ $this->assertStringNotContainsString('1 test1 and another2.
-another ref to the first note1.
+another ref to the first note1.