FIX: Prevent infinite loop when replacing watched words (#12967)

FIX: Prevent infinite loop when replacing watched words (#12967)

diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-watched-word.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-watched-word.hbs
index 0583291..cc03496 100644
--- a/app/assets/javascripts/admin/addon/templates/components/admin-watched-word.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/admin-watched-word.hbs
@@ -1 +1 @@
-{{d-icon "times"}} {{word.word}} {{#if word.replacement}}→ {{word.replacement}}{{/if}}
+{{d-icon "times"}} {{word.word}} {{#if word.replacement}}&rarr; <span class="replacement">{{word.replacement}}</span>{{/if}}
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 3d6d500..f413ffe 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
@@ -1672,4 +1672,27 @@ var bar = 'bar';
     assert.cookedOptions("(r) (R)", enabledTypographer, "<p>(r) (R)</p>");
     assert.cookedOptions("(p) (P)", enabledTypographer, "<p>(p) (P)</p>");
   });
+
+  test("watched words replace", function (assert) {
+    const opts = {
+      watchedWordsReplacements: { fun: "times" },
+    };
+
+    assert.cookedOptions("test fun", opts, "<p>test times</p>");
+  });
+
+  test("watched words replace with bad regex", function (assert) {
+    const maxMatches = 100; // same limit as MD watched-words-replace plugin
+    const opts = {
+      siteSettings: { watched_words_regular_expressions: true },
+      watchedWordsReplacements: { "\\bu?\\b": "you" },
+    };
+
+    assert.cookedOptions(
+      "one",
+      opts,
+      `<p>${"you".repeat(maxMatches)}one</p>`,
+      "does not loop infinitely"
+    );
+  });
 });
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/watched-words-replace.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/watched-words-replace.js
index f006efb..7c358d1 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/watched-words-replace.js
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/watched-words-replace.js
@@ -10,9 +10,17 @@ function findAllMatches(text, matchers, useRegExp) {
   const matches = [];
 
   if (useRegExp) {
+    const maxMatches = 100;
+    let index = 0;
+
     matchers.forEach((matcher) => {
       let match;
-      while ((match = matcher.pattern.exec(text)) !== null) {
+      while (
+        (match = matcher.pattern.exec(text)) !== null &&
+        index < maxMatches
+      ) {
+        index++;
+
         matches.push({
           index: match.index,
           text: match[0],
diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss
index 9d70972..4c9c995 100644
--- a/app/assets/stylesheets/common/admin/staff_logs.scss
+++ b/app/assets/stylesheets/common/admin/staff_logs.scss
@@ -330,6 +330,10 @@ table.screened-ip-addresses {
   width: 250px;
   margin-bottom: 1em;
   vertical-align: top;
+  .replacement {
+    white-space: pre;
+    background-color: var(--tertiary-low);
+  }
 }
 
 .watched-words-uploader {

GitHub sha: b61d4663

This commit appears in #12967 which was approved by eviltrout. It was merged by pmusaraj.