FIX: Generate unique HTML heading names (#12705)

FIX: Generate unique HTML heading names (#12705)

Headings with the exact same name generated exactly the same heading names, which was invalid. This replaces the old code for generating names for non-English headings which were using URI encode and resulted in unreadable headings.

diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
index 0d42988..3d6d500 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
@@ -670,7 +670,7 @@ eviltrout</p>
 
     assert.cooked(
       "# #category-hashtag",
-      '<h1><a name="category-hashtag" class="anchor" href="#category-hashtag"></a><span class="hashtag">#category-hashtag</span></h1>',
+      '<h1><a name="category-hashtag-1" class="anchor" href="#category-hashtag-1"></a><span class="hashtag">#category-hashtag</span></h1>',
       "it works within ATX-style headers"
     );
 
@@ -696,7 +696,7 @@ eviltrout</p>
   test("Heading", function (assert) {
     assert.cooked(
       "**Bold**\n----------",
-      '<h2><a name="bold" class="anchor" href="#bold"></a><strong>Bold</strong></h2>',
+      '<h2><a name="bold-1" class="anchor" href="#bold-1"></a><strong>Bold</strong></h2>',
       "It will bold the heading"
     );
   });
@@ -939,7 +939,7 @@ eviltrout</p>
 
     assert.cooked(
       "## a\nb\n`‍``\nc\n`‍``",
-      '<h2><a name="a" class="anchor" href="#a"></a>a</h2>\n<p>b</p>\n<pre><code class="lang-auto">c\n</code></pre>',
+      '<h2><a name="a-1" class="anchor" href="#a-1"></a>a</h2>\n<p>b</p>\n<pre><code class="lang-auto">c\n</code></pre>',
       "it handles headings with code blocks after them."
     );
   });
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js
index 03941e6..4ac1920 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js
@@ -1,5 +1,3 @@
-const SPECIAL_CHARACTERS_REGEX = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~’]/g;
-
 export function setup(helper) {
   if (helper.getOptions().previewing) {
     return;
@@ -7,7 +5,11 @@ export function setup(helper) {
 
   helper.registerPlugin((md) => {
     md.core.ruler.push("anchor", (state) => {
-      for (let idx = 0, lvl = 0; idx < state.tokens.length; idx++) {
+      for (
+        let idx = 0, lvl = 0, headingId = 0;
+        idx < state.tokens.length;
+        idx++
+      ) {
         if (
           state.tokens[idx].type === "blockquote_open" ||
           (state.tokens[idx].type === "bbcode_open" &&
@@ -37,15 +39,7 @@ export function setup(helper) {
           .replace(/^-+/, "")
           .replace(/-+$/, "");
 
-        if (slug.length === 0) {
-          slug = state.tokens[idx + 1].content
-            .replace(/\s+/g, "-")
-            .replace(SPECIAL_CHARACTERS_REGEX, "")
-            .replace(/\-\-+/g, "-")
-            .replace(/^-+/, "")
-            .replace(/-+$/, "");
-          slug = encodeURI(slug).replace(/%/g, "").substr(0, 24);
-        }
+        slug = `${slug || "heading"}-${++headingId}`;
 
         linkOpen.attrSet("name", slug);
         linkOpen.attrSet("class", "anchor");
diff --git a/plugins/poll/spec/lib/pretty_text_spec.rb b/plugins/poll/spec/lib/pretty_text_spec.rb
index c5b25e0..52fed22 100644
--- a/plugins/poll/spec/lib/pretty_text_spec.rb
+++ b/plugins/poll/spec/lib/pretty_text_spec.rb
@@ -189,8 +189,8 @@ describe PrettyText do
       </div>
     HTML
 
-    expect(cooked).to include("<h1>\n<a name=\"pre-heading\" class=\"anchor\" href=\"#pre-heading\"></a>Pre-heading</h1>")
-    expect(cooked).to include("<h1>\n<a name=\"post-heading\" class=\"anchor\" href=\"#post-heading\"></a>Post-heading</h1>")
+    expect(cooked).to include("<h1>\n<a name=\"pre-heading-1\" class=\"anchor\" href=\"#pre-heading-1\"></a>Pre-heading</h1>")
+    expect(cooked).to include("<h1>\n<a name=\"post-heading-2\" class=\"anchor\" href=\"#post-heading-2\"></a>Post-heading</h1>")
   end
 
   it "does not break when there are headings before/after a poll without a title" do
@@ -211,7 +211,7 @@ describe PrettyText do
       <div class="poll" data-poll-status="open" data-poll-name="poll">
     HTML
 
-    expect(cooked).to include("<h1>\n<a name=\"pre-heading\" class=\"anchor\" href=\"#pre-heading\"></a>Pre-heading</h1>")
-    expect(cooked).to include("<h1>\n<a name=\"post-heading\" class=\"anchor\" href=\"#post-heading\"></a>Post-heading</h1>")
+    expect(cooked).to include("<h1>\n<a name=\"pre-heading-1\" class=\"anchor\" href=\"#pre-heading-1\"></a>Pre-heading</h1>")
+    expect(cooked).to include("<h1>\n<a name=\"post-heading-2\" class=\"anchor\" href=\"#post-heading-2\"></a>Post-heading</h1>")
   end
 end
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 6cd371a..3baaa24 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -1909,7 +1909,7 @@ HTML
 
     html = <<~HTML
       <h1>
-      <a name="hello-world" class="anchor" href="#hello-world"></a>
+      <a name="hello-world-1" class="anchor" href="#hello-world-1"></a>
       Hello world
       </h1>
     HTML

GitHub sha: 96a16123

This commit appears in #12705 which was approved by eviltrout. It was merged by nbianca.