Repository: thephpleague/commonmark (php)

Issue:
Footnote ID prefix and footnote ref ID prefix config options sometimes ignored
**Version(s) affected**: 1.5.x

**Description**  
<!-- A clear and concise description of the problem. -->

The `footnote/ref_id_prefix` and `footnote/footnote_id_prefix` configuration options are only used to generate element `id` attributes, but are ignored for the `href` attributes corresponding to those IDs, breaking all footnote reference links and backlinks if either config option is customized.

The default values for these configuration options, `fnref:` and `fn:`, are hard-coded in 5 places:

https://github.com/thephpleague/commonmark/blob/d9fdf5f5ea370c988375e54c85a43b2668a048e6/src/Extension/Footnote/Event/GatherFootnotesListener.php#L52

https://github.com/thephpleague/commonmark/blob/d9fdf5f5ea370c988375e54c85a43b2668a048e6/src/Extension/Footnote/Event/GatherFootnotesListener.php#L95

https://github.com/thephpleague/commonmark/blob/d9fdf5f5ea370c988375e54c85a43b2668a048e6/src/Extension/Footnote/Event/AnonymousFootnotesListener.php#L46

https://github.com/thephpleague/commonmark/blob/d9fdf5f5ea370c988375e54c85a43b2668a048e6/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php#L80

https://github.com/thephpleague/commonmark/blob/d9fdf5f5ea370c988375e54c85a43b2668a048e6/src/Extension/Footnote/Parser/FootnoteRefParser.php#L59

**How to reproduce**  
<!-- Markdown and/or config needed to reproduce the problem. Don't forget to mention any extensions you might have enabled! -->

Config:

```php
    'footnote' => [
        'ref_id_prefix' => 'customfnref:',
        'footnote_id_prefix' => 'customfn:',
    ],
```

Markdown:

```md
Here[^note1]

[^note1]: There
```

Expected HTML:

```html
<p>Here<sup id="customfnref:note1"><a class="custom-ref" href="#customfn:note1" role="doc-noteref">1</a></sup></p>
<div class="custom-notes" role="doc-endnotes">
    <ol>
        <li class="custom-footnote" id="customfn:note1" role="doc-endnote">
            <p>There&nbsp;<a class="custom-backref" rev="footnote" href="#customfnref:note1" role="doc-backlink">↩</a></p>
        </li>
    </ol>
</div>
```

Actual HTML:

```html
<!-- Correct id, wrong href -->
<p>Here<sup id="customfnref:note1"><a class="custom-ref" href="#fn:note1" role="doc-noteref">1</a></sup></p>
<div class="custom-notes" role="doc-endnotes">
    <ol>
        <!-- Correct id -->
        <li class="custom-footnote" id="customfn:note1" role="doc-endnote">
            <!-- Wrong href -->
            <p>There&nbsp;<a class="custom-backref" rev="footnote" href="#fnref:note1" role="doc-backlink">↩</a></p>
        </li>
    </ol>
</div>
```

**Possible Solution**  
<!--- Optional: only if you have suggestions on a fix/reason for the bug -->

Instead of hard-coding these values, we can update those 5 references to them to check the config. I'm not sure how involved this would be since it looks to me like none of those classes currently have access to the config at all.

But also, since these attributes are completely hidden from end users and don't affect styling, it occurred to me that they don't necessarily need to be customizable? An easier solution would be to hard-code them everywhere and not allow them to be overriden at all.

Thoughts? Either way, I'll try to get a PR in to fix this in the next week or two!


Pull request description:
Fix hard-coded footnote ID config
This PR fixes #524 by removing hard-coded `fn:` and `fnref:` and ensuring that the `footnote/footnote_id_prefix` and `footnote/footnote_ref_prefix` configuration options are always used.

It updates the `AnonymousFootnotesListener`, `GatherFootnotesListener`, `AnonymousFootnoteRefParser`, and `FootnoteRefParser` classes to all be configuration-aware.

I added an integration test in the style of [`Strikethrough/IntegrationTest`](https://github.com/thephpleague/commonmark/blob/latest/tests/unit/Extension/Strikethrough/IntegrationTest.php) to make sure this works, I tried to add unit tests but I couldn't figure out how to render both a footnote _and_ its reference. I'm happy to add more tests if someone can point me in the right direction.

Patch:
diff --git a/.styleci.yml b/.styleci.yml
index af07887d..f86d6e6e 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -1,5 +1,7 @@
 preset: recommended
 
+risky: true
+
 enabled:
   - concat_with_spaces
   - no_useless_else
diff --git a/src/Extension/Footnote/Event/AnonymousFootnotesListener.php b/src/Extension/Footnote/Event/AnonymousFootnotesListener.php
index a1ff53cf..304e6860 100644
--- a/src/Extension/Footnote/Event/AnonymousFootnotesListener.php
+++ b/src/Extension/Footnote/Event/AnonymousFootnotesListener.php
@@ -21,9 +21,14 @@ use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
 use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
 use League\CommonMark\Inline\Element\Text;
 use League\CommonMark\Reference\Reference;
+use League\CommonMark\Util\ConfigurationAwareInterface;
+use League\CommonMark\Util\ConfigurationInterface;
 
-final class AnonymousFootnotesListener
+final class AnonymousFootnotesListener implements ConfigurationAwareInterface
 {
+    /** @var ConfigurationInterface */
+    private $config;
+
     public function onDocumentParsed(DocumentParsedEvent $event): void
     {
         $document = $event->getDocument();
@@ -36,7 +41,7 @@ final class AnonymousFootnotesListener
                 $existingReference = $node->getReference();
                 $reference = new Reference(
                     $existingReference->getLabel(),
-                    '#fnref:' . $existingReference->getLabel(),
+                    '#' . $this->config->get('footnote/ref_id_prefix', 'fnref:') . $existingReference->getLabel(),
                     $existingReference->getTitle()
                 );
                 $footnote = new Footnote($reference);
@@ -48,4 +53,9 @@ final class AnonymousFootnotesListener
             }
         }
     }
+
+    public function setConfiguration(ConfigurationInterface $config): void
+    {
+        $this->config = $config;
+    }
 }
diff --git a/src/Extension/Footnote/Event/GatherFootnotesListener.php b/src/Extension/Footnote/Event/GatherFootnotesListener.php
index 3834b30f..a54ca26d 100644
--- a/src/Extension/Footnote/Event/GatherFootnotesListener.php
+++ b/src/Extension/Footnote/Event/GatherFootnotesListener.php
@@ -20,9 +20,14 @@ use League\CommonMark\Extension\Footnote\Node\Footnote;
 use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
 use League\CommonMark\Extension\Footnote\Node\FootnoteContainer;
 use League\CommonMark\Reference\Reference;
+use League\CommonMark\Util\ConfigurationAwareInterface;
+use League\CommonMark\Util\ConfigurationInterface;
 
-final class GatherFootnotesListener
+final class GatherFootnotesListener implements ConfigurationAwareInterface
 {
+    /** @var ConfigurationInterface */
+    private $config;
+
     public function onDocumentParsed(DocumentParsedEvent $event): void
     {
         $document = $event->getDocument();
@@ -53,12 +58,15 @@ final class GatherFootnotesListener
              * Look for all footnote refs pointing to this footnote
              * and create each footnote backrefs.
              */
-            $backrefs = $document->getData('#fn:' . $node->getReference()->getDestination(), []);
+            $backrefs = $document->getData(
+                '#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $node->getReference()->getDestination(),
+                []
+            );
             /** @var Reference $backref */
             foreach ($backrefs as $backref) {
                 $node->addBackref(new FootnoteBackref(new Reference(
                     $backref->getLabel(),
-                    '#fnref:' . $backref->getLabel(),
+                    '#' . $this->config->get('footnote/ref_id_prefix', 'fnref:') . $backref->getLabel(),
                     $backref->getTitle()
                 )));
             }
@@ -84,4 +92,9 @@ final class GatherFootnotesListener
 
         return $footnoteContainer;
     }
+
+    public function setConfiguration(ConfigurationInterface $config): void
+    {
+        $this->config = $config;
+    }
 }
diff --git a/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php b/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php
index cf7f2096..525bbfc9 100644
--- a/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php
+++ b/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php
@@ -20,9 +20,14 @@ use League\CommonMark\InlineParserContext;
 use League\CommonMark\Normalizer\SlugNormalizer;
 use League\CommonMark\Normalizer\TextNormalizerInterface;
 use League\CommonMark\Reference\Reference;
+use League\CommonMark\Util\ConfigurationAwareInterface;
+use League\CommonMark\Util\ConfigurationInterface;
 
-final class AnonymousFootnoteRefParser implements InlineParserInterface
+final class AnonymousFootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
 {
+    /** @var ConfigurationInterface */
+    private $config;
+
     /** @var TextNormalizerInterface */
     private $slugNormalizer;
 
@@ -66,6 +71,15 @@ final class AnonymousFootnoteRefParser implements InlineParserInterface
         $refLabel = $this->slugNormalizer->normalize($label);
         $refLabel = \mb_substr($refLabel, 0, 20);
 
-        return new Reference($refLabel, '#fn:' . $refLabel, $label);
+        return new Reference(
+            $refLabel,
+            '#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $refLabel,
+            $label
+        );
+    }
+
+    public function setConfiguration(ConfigurationInterface $config): void
+    {
+        $this->config = $config;
     }
 }
diff --git a/src/Extension/Footnote/Parser/FootnoteRefParser.php b/src/Extension/Footnote/Parser/FootnoteRefParser.php
index 496124e7..313b5dd2 100644
--- a/src/Extension/Footnote/Parser/FootnoteRefParser.php
+++ b/src/Extension/Footnote/Parser/FootnoteRefParser.php
@@ -18,9 +18,14 @@ use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
 use League\CommonMark\Inline\Parser\InlineParserInterface;
 use League\CommonMark\InlineParserContext;
 use League\CommonMark\Reference\Reference;
+use League\CommonMark\Util\ConfigurationAwareInterface;
+use League\CommonMark\Util\ConfigurationInterface;
 
-final class FootnoteRefParser implements InlineParserInterface
+final class FootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
 {
+    /** @var ConfigurationInterface */
+    private $config;
+
     public function getCharacters(): array
     {
         return ['['];
@@ -53,6 +58,15 @@ final class FootnoteRefParser implements InlineParserInterface
 
     private function createReference(string $label): Reference
     {
-        return new Reference($label, '#fn:' . $label, $label);
+        return new Reference(
+            $label,
+            '#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $label,
+            $label
+        );
+    }
+
+    public function setConfiguration(ConfigurationInterface $config): void
+    {
+        $this->config = $config;
     }
 }
diff --git a/src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php b/src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php
index aaf13554..ebdb7097 100644
--- a/src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php
+++ b/src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php
@@ -18,8 +18,8 @@ use League\CommonMark\Extension\HeadingPermalink\Slug\SlugGeneratorInterface as
 use League\CommonMark\Inline\Element\Code;
 use League\CommonMark\Inline\Element\Text;
 use League\CommonMark\Node\Node;
-use League\CommonMark\Normalizer\SlugNormalizer;
 use League\CommonMark\Normalizer\TextNormalizerInterface;
+use League\CommonMark\Normalizer\UniqueSlugNormalizer;
 use League\CommonMark\Util\ConfigurationAwareInterface;
 use League\CommonMark\Util\ConfigurationInterface;
 
@@ -46,7 +46,7 @@ final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
             @trigger_error(sprintf('Passing a %s into the %s constructor is deprecated; use a %s instead', DeprecatedSlugGeneratorInterface::class, self::class, TextNormalizerInterface::class), E_USER_DEPRECATED);
         }
 
-        $this->slugNormalizer = $slugNormalizer ?? new SlugNormalizer();
+        $this->slugNormalizer = $slugNormalizer ?? new UniqueSlugNormalizer();
     }
 
     public function setConfiguration(ConfigurationInterface $configuration)
diff --git a/src/Normalizer/UniqueSlugNormalizer.php b/src/Normalizer/UniqueSlugNormalizer.php
new file mode 100644
index 00000000..4c87f084
--- /dev/null
+++ b/src/Normalizer/UniqueSlugNormalizer.php
@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the league/commonmark package.
+ *
+ * (c) Colin O'Dell <colinodell@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace League\CommonMark\Normalizer;
+
+final class UniqueSlugNormalizer implements TextNormalizerInterface
+{
+    /** @var SlugNormalizer */
+    private $slugNormalizer;
+
+    /** @var array<string, int> */
+    private $usages;
+
+    public function __construct()
+    {
+        $this->slugNormalizer = new SlugNormalizer();
+    }
+
+    public function normalize(string $text, $context = null): string
+    {
+        $slug = $this->slugNormalizer->normalize($text, $context);
+
+        if (!isset($this->usages[$slug])) {
+            $this->usages[$slug] = 1;
+
+            return $slug;
+        }
+
+        $count = $this->usages[$slug]++;
+
+        return $slug . '-' . $count;
+    }
+}


Test patch:
diff --git a/tests/functional/Extension/HeadingPermalink/HeadingPermalinkExtensionTest.php b/tests/functional/Extension/HeadingPermalink/HeadingPermalinkExtensionTest.php
index fa32e388..2e601dc3 100644
--- a/tests/functional/Extension/HeadingPermalink/HeadingPermalinkExtensionTest.php
+++ b/tests/functional/Extension/HeadingPermalink/HeadingPermalinkExtensionTest.php
@@ -41,6 +41,7 @@ final class HeadingPermalinkExtensionTest extends TestCase
         yield ['# Hello *World*', sprintf('<h1><a id="user-content-hello-world" href="#hello-world" name="hello-world" class="heading-permalink" aria-hidden="true" title="Permalink">%s</a>Hello <em>World</em></h1>', HeadingPermalinkRenderer::DEFAULT_SYMBOL)];
         yield ['# Hello `World`', sprintf('<h1><a id="user-content-hello-world" href="#hello-world" name="hello-world" class="heading-permalink" aria-hidden="true" title="Permalink">%s</a>Hello <code>World</code></h1>', HeadingPermalinkRenderer::DEFAULT_SYMBOL)];
         yield ["Test\n----", sprintf('<h2><a id="user-content-test" href="#test" name="test" class="heading-permalink" aria-hidden="true" title="Permalink">%s</a>Test</h2>', HeadingPermalinkRenderer::DEFAULT_SYMBOL)];
+        yield ["# Hello World!\n\n# Hello World!", sprintf("<h1><a id=\"user-content-hello-world\" href=\"#hello-world\" name=\"hello-world\" class=\"heading-permalink\" aria-hidden=\"true\" title=\"Permalink\">%s</a>Hello World!</h1>\n<h1><a id=\"user-content-hello-world-1\" href=\"#hello-world-1\" name=\"hello-world-1\" class=\"heading-permalink\" aria-hidden=\"true\" title=\"Permalink\">%s</a>Hello World!</h1>", HeadingPermalinkRenderer::DEFAULT_SYMBOL, HeadingPermalinkRenderer::DEFAULT_SYMBOL)];
     }
 
     /**
diff --git a/tests/functional/Extension/TableOfContents/data/weird-relative.html b/tests/functional/Extension/TableOfContents/data/weird-relative.html
index 90c0c6e3..4aed5c76 100644
--- a/tests/functional/Extension/TableOfContents/data/weird-relative.html
+++ b/tests/functional/Extension/TableOfContents/data/weird-relative.html
@@ -22,10 +22,10 @@
 </ul>
 </li>
 <li>
-<p><a href="#then-a-level-5">Then a level 5</a></p>
+<p><a href="#then-a-level-5-1">Then a level 5</a></p>
 <ul>
 <li>
-<p><a href="#then-a-level-6">Then a level 6</a></p>
+<p><a href="#then-a-level-6-1">Then a level 6</a></p>
 </li>
 </ul>
 </li>
@@ -35,7 +35,7 @@
 <p><a href="#and-then-a-level-3">And then a level 3</a></p>
 <ul>
 <li>
-<p><a href="#then-a-level-5">Then a level 5</a></p>
+<p><a href="#then-a-level-5-2">Then a level 5</a></p>
 </li>
 </ul>
 </li>
@@ -71,7 +71,7 @@
 <p><a href="#level-2">Level 2</a></p>
 <ul>
 <li>
-<p><a href="#level-4">Level 4</a></p>
+<p><a href="#level-4-1">Level 4</a></p>
 </li>
 </ul>
 </li>
@@ -131,10 +131,10 @@
 <h2><a id="user-content-and-another-level-2" href="#and-another-level-2" name="and-another-level-2" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>And another level 2</h2>
 <h5><a id="user-content-then-a-level-5" href="#then-a-level-5" name="then-a-level-5" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 5</h5>
 <h6><a id="user-content-then-a-level-6" href="#then-a-level-6" name="then-a-level-6" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 6</h6>
-<h5><a id="user-content-then-a-level-5" href="#then-a-level-5" name="then-a-level-5" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 5</h5>
-<h6><a id="user-content-then-a-level-6" href="#then-a-level-6" name="then-a-level-6" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 6</h6>
+<h5><a id="user-content-then-a-level-5-1" href="#then-a-level-5-1" name="then-a-level-5-1" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 5</h5>
+<h6><a id="user-content-then-a-level-6-1" href="#then-a-level-6-1" name="then-a-level-6-1" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 6</h6>
 <h3><a id="user-content-and-then-a-level-3" href="#and-then-a-level-3" name="and-then-a-level-3" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>And then a level 3</h3>
-<h5><a id="user-content-then-a-level-5" href="#then-a-level-5" name="then-a-level-5" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 5</h5>
+<h5><a id="user-content-then-a-level-5-2" href="#then-a-level-5-2" name="then-a-level-5-2" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 5</h5>
 <h2><a id="user-content-then-a-level-2" href="#then-a-level-2" name="then-a-level-2" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then a level 2</h2>
 <h6><a id="user-content-followed-by-a-level-6" href="#followed-by-a-level-6" name="followed-by-a-level-6" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Followed by a level 6</h6>
 <h3><a id="user-content-then-another-level-3" href="#then-another-level-3" name="then-another-level-3" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Then another level 3</h3>
@@ -142,7 +142,7 @@
 <h4><a id="user-content-level-4" href="#level-4" name="level-4" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Level 4</h4>
 <h3><a id="user-content-level-3" href="#level-3" name="level-3" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Level 3</h3>
 <h2><a id="user-content-level-2" href="#level-2" name="level-2" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Level 2</h2>
-<h4><a id="user-content-level-4" href="#level-4" name="level-4" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Level 4</h4>
+<h4><a id="user-content-level-4-1" href="#level-4-1" name="level-4-1" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Level 4</h4>
 <h1><a id="user-content-now-for-some-normal-headings-to-test-going-up-and-down-the-stack" href="#now-for-some-normal-headings-to-test-going-up-and-down-the-stack" name="now-for-some-normal-headings-to-test-going-up-and-down-the-stack" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Now for some &quot;normal&quot; headings to test going up and down the stack</h1>
 <h1><a id="user-content-section-1" href="#section-1" name="section-1" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Section 1</h1>
 <h2><a id="user-content-section-11" href="#section-11" name="section-11" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Section 1.1</h2>
diff --git a/tests/unit/EnvironmentTest.php b/tests/unit/EnvironmentTest.php
index 0beacb02..9ff80aa1 100644
--- a/tests/unit/EnvironmentTest.php
+++ b/tests/unit/EnvironmentTest.php
@@ -594,7 +594,7 @@ class EnvironmentTest extends TestCase
             $actualOrder[] = 'c';
         }, 10);
 
-        $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder) {
+        $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) {
             $this->fail('Propogation should have been stopped before here');
         });
 
diff --git a/tests/unit/Extension/Footnote/IntegrationTest.php b/tests/unit/Extension/Footnote/IntegrationTest.php
new file mode 100644
index 00000000..5878df2c
--- /dev/null
+++ b/tests/unit/Extension/Footnote/IntegrationTest.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the league/commonmark package.
+ *
+ * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace League\CommonMark\Tests\Unit\Extension\Footnote;
+
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment;
+use League\CommonMark\Extension\Footnote\FootnoteExtension;
+use PHPUnit\Framework\TestCase;
+
+class IntegrationTest extends TestCase
+{
+    /**
+     * @dataProvider dataForIntegrationTest
+     */
+    public function testFootnote(string $string, string $expected, array $config = []): void
+    {
+        $environment = Environment::createCommonMarkEnvironment();
+        $environment->addExtension(new FootnoteExtension([
+            'ref_id_prefix' => 'custom-ref-',
+            'footnote_id_prefix' => 'custom-',
+        ]));
+
+        $converter = new CommonMarkConverter(['footnote' => $config], $environment);
+
+        $html = \trim($converter->convertToHtml($string));
+
+        $this->assertSame($expected, $html);
+    }
+
+    /**
+     * @return array<array<string>>
+     */
+    public function dataForIntegrationTest(): array
+    {
+        return [
+            [
+                "Here[^note1]\n\n[^note1]: There",
+                '<p>Here<sup id="fnref:note1"><a class="footnote-ref" href="#fn:note1" role="doc-noteref">1</a></sup></p>
+<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:note1" role="doc-endnote"><p>There&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:note1" role="doc-backlink">&#8617;</a></p></li></ol></div>',
+            ],
+            [
+                "Here[^note1]\n\n[^note1]: There",
+                '<p>Here<sup id="customfnref:note1"><a class="footnote-ref" href="#customfn:note1" role="doc-noteref">1</a></sup></p>
+<div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="customfn:note1" role="doc-endnote"><p>There&nbsp;<a class="footnote-backref" rev="footnote" href="#customfnref:note1" role="doc-backlink">&#8617;</a></p></li></ol></div>',
+                ['ref_id_prefix' => 'customfnref:', 'footnote_id_prefix' => 'customfn:'],
+            ],
+        ];
+    }
+}
diff --git a/tests/unit/Normalizer/UniqueSlugNormalizerTest.php b/tests/unit/Normalizer/UniqueSlugNormalizerTest.php
new file mode 100644
index 00000000..6a45ca7e
--- /dev/null
+++ b/tests/unit/Normalizer/UniqueSlugNormalizerTest.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace League\CommonMark\Tests\Unit\Normalizer;
+
+use League\CommonMark\Normalizer\UniqueSlugNormalizer;
+use PHPUnit\Framework\TestCase;
+
+final class UniqueSlugNormalizerTest extends TestCase
+{
+    public function testNormalize(): void
+    {
+        $normalizer = new UniqueSlugNormalizer();
+
+        // Providing the same input multiple times should give different slugs
+        $this->assertSame('test', $normalizer->normalize('Test'));
+        $this->assertSame('test-1', $normalizer->normalize('Test'));
+        $this->assertSame('test-2', $normalizer->normalize('Test'));
+
+        // This behavior should also work for different inputs that normally produce the same slug
+        $this->assertSame('hello-world', $normalizer->normalize('hello world'));
+        $this->assertSame('hello-world-1', $normalizer->normalize('hello-world'));
+        $this->assertSame('hello-world-2', $normalizer->normalize('Hello, World!'));
+    }
+}