FIX: Correctly toggle checkboxes when used with text modifiers or codeblocks (#17)

FIX: Correctly toggle checkboxes when used with text modifiers or codeblocks (#17)

  • FIX: Correctly toggle checkboxes following various text modifiers

  • FIX: Correctly toggle checkboxes preceding codeblocks

diff --git a/assets/javascripts/discourse/initializers/checklist.js.es6 b/assets/javascripts/discourse/initializers/checklist.js.es6
index 2c4db8f..8dcd1de 100644
--- a/assets/javascripts/discourse/initializers/checklist.js.es6
+++ b/assets/javascripts/discourse/initializers/checklist.js.es6
@@ -39,7 +39,10 @@ export function checklistSyntax($elem, post) {
           [
             /`[^`\n]*\n?[^`\n]*`/gm,
             /^`‍``[^]*?^`‍``/gm,
-            /\[code\][^]*?\[\/code\]/gm
+            /\[code\][^]*?\[\/code\]/gm,
+            /_.*?_/gm,
+            /\*.*?\*/gm,
+            /~~.*?~~/gm,
           ].forEach(regex => {
             let match;
             while ((match = regex.exec(result.raw)) != null) {
@@ -49,11 +52,22 @@ export function checklistSyntax($elem, post) {
 
           // make the first run go to index = 0
           let nth = -1;
+          let found = false;
           const newRaw = result.raw.replace(
             /\[(\s|\_|\-|\x|\\?\*)?\]/gi,
             (match, ignored, off) => {
+              if (found) {
+                return match;
+              }
+
               nth += blocks.every(b => b[0] > off + match.length || off > b[1]);
-              return nth === idx ? newValue : match;
+
+              if (nth === idx) {
+                found = true; // Do not replace any further matches
+                return newValue;
+              }
+
+              return match;
             }
           );
 
diff --git a/spec/pretty_text_spec.rb b/spec/pretty_text_spec.rb
index 2eb6309..647e283 100644
--- a/spec/pretty_text_spec.rb
+++ b/spec/pretty_text_spec.rb
@@ -8,12 +8,12 @@ describe PrettyText do
     it 'can properly bake boxes' do
       md = <<~MD
         [],[ ],[_];[-]X[x]X [*] [\\*] are all checkboxes
-        `[ ]` [x](hello) *[ ]* **[ ]** are not checkboxes
+        `[ ]` [x](hello) *[ ]* **[ ]** _[ ]_ __[ ]__ ~~[ ]~~ are not checkboxes
       MD
 
       html = <<~HTML
         <p><span class="chcklst-box fa fa-square-o fa-fw"></span>,<span class="chcklst-box fa fa-square-o fa-fw"></span>,<span class="chcklst-box fa fa-square fa-fw"></span>;<span class="chcklst-box fa fa-minus-square-o fa-fw"></span>X<span class="chcklst-box checked fa fa-check-square fa-fw"></span>X <span class="chcklst-box checked fa fa-check-square-o fa-fw"></span> <span class="chcklst-box checked fa fa-check-square-o fa-fw"></span> are all checkboxes<br>
-        <code>[ ]</code> <a>x</a> <em>[ ]</em> <strong>[ ]</strong> are not checkboxes</p>
+        <code>[ ]</code> <a>x</a> <em>[ ]</em> <strong>[ ]</strong> <em>[ ]</em> <strong>[ ]</strong> <s>[ ]</s> are not checkboxes</p>
       HTML
       cooked = PrettyText.cook(md)
       expect(cooked).to eq(html.strip)
diff --git a/test/javascripts/lib/checklist-test.js.es6 b/test/javascripts/lib/checklist-test.js.es6
index b66e69d..e217607 100644
--- a/test/javascripts/lib/checklist-test.js.es6
+++ b/test/javascripts/lib/checklist-test.js.es6
@@ -3,12 +3,97 @@ import { checklistSyntax } from "discourse/plugins/discourse-checklist/discourse
 
 QUnit.module("initializer:checklist");
 
-QUnit.test("correct checkbox is selected", assert => {
-  const raw = `Hi there,
+QUnit.test("checkbox before a code block", assert => {
+  const raw = `
+[ ] first
+[*] actual
+\`[x] nope\`
+  `;
+
+  const cooked = `<div class="cooked">
+<p><span class="chcklst-box fa fa-square-o fa-fw" style="cursor: pointer;"></span> first<br>
+<span class="chcklst-box checked fa fa-check-square-o fa-fw" style="cursor: pointer;"></span> actual</p>
+<pre>[x] nope</pre>
+</div>
+  `;
+
+  const model = Post.create({ id: 42, can_edit: true });
+  const decoratorHelper = { getModel: () => model };
+  const $elem = $(cooked);
 
-It seems that a code block followed by a checklist breaks things.
+  // eslint-disable-next-line no-undef
+  server.get("/posts/42", () => [
+    200,
+    { "Content-Type": "application/json" },
+    { raw: raw }
+  ]);
+
+  checklistSyntax($elem, decoratorHelper);
+
+  const done = assert.async();
+  model.save = fields => {
+    assert.ok(fields.raw.includes("[ ] first"));
+    assert.ok(fields.raw.includes("[ ] actual"));
+    assert.ok(fields.raw.includes("[x] nope"));
+
+    done();
+  };
+
+  $elem.find(".chcklst-box:nth(1)").click();
+});
+
+QUnit.test("checkbox before a multiline code block", assert => {
+  const raw = `
+[ ] first
+[*] actual
+\`\`\`
+[x] nope
+[x] neither
+\`\`\`
+  `;
+
+  const cooked = `<div class="cooked">
+<p><span class="chcklst-box fa fa-square-o fa-fw" style="cursor: pointer;"></span> first<br>
+<span class="chcklst-box checked fa fa-check-square-o fa-fw" style="cursor: pointer;"></span> actual</p>
+<pre><code>[x]
+[x]
+</code></pre>
+</div>
+  `;
 
+  const model = Post.create({ id: 42, can_edit: true });
+  const decoratorHelper = { getModel: () => model };
+  const $elem = $(cooked);
+
+  // eslint-disable-next-line no-undef
+  server.get("/posts/42", () => [
+    200,
+    { "Content-Type": "application/json" },
+    { raw: raw }
+  ]);
+
+  checklistSyntax($elem, decoratorHelper);
+
+  const done = assert.async();
+  model.save = fields => {
+    assert.ok(fields.raw.includes("[ ] first"));
+    assert.ok(fields.raw.includes("[ ] actual"));
+    assert.ok(fields.raw.includes("[x] nope"));
+
+    done();
+  };
+
+  $elem.find(".chcklst-box:nth(1)").click();
+});
+
+QUnit.test("correct checkbox is selected", assert => {
+  const raw = `
 \`[x]\`
+*[x]*
+**[x]**
+_[x]_
+__[x]__
+~~[x]~~
 
 [code]
 [\*]
@@ -23,18 +108,21 @@ It seems that a code block followed by a checklist breaks things.
 [ ]
 [\*]
 \`\`\`
-Will create a list like this:
+
+Actual checkboxes:
 [] first
 [*] second
 [x] third
 [_] fourth
-
-Clicking the boxes will ruin the code block and the list becomes unresponsive.`;
+`;
 
   const cooked = `<div class="cooked">
-<p>Hi there,</p>
-<p>It seems that a code block followed by a checklist breaks things.</p>
 <pre>[*]</pre>
+<em>[*]</em>
+<strong>[*]</strong>
+<em>[*]</em>
+<strong>[*]</strong>
+<s>[*]</s>
 <pre><code>[\*]
 [ ]
 [ ]
@@ -45,18 +133,16 @@ Clicking the boxes will ruin the code block and the list becomes unresponsive.`;
 [ ]
 [\*]
 </code></pre>
-<p>Will create a list like this:<br>
-<span class="chcklst-box fa fa-square-o fa-fw" style="cursor: pointer;"></span><br>
-<span class="chcklst-box checked fa fa-check-square-o fa-fw" style="cursor: pointer;"></span><br>
-<span class="chcklst-box checked fa fa-check-square fa-fw" style="cursor: pointer;"></span><br>
-<span class="chcklst-box fa fa-square fa-fw" style="cursor: pointer;"></span></p>
-<p>Clicking the boxes will ruin the code block and the list becomes unresponsive.</p>
+<p>Actual checkboxes:<br>
+<span class="chcklst-box fa fa-square-o fa-fw" style="cursor: pointer;"></span> first<br>
+<span class="chcklst-box checked fa fa-check-square-o fa-fw" style="cursor: pointer;"></span> second<br>
+<span class="chcklst-box checked fa fa-check-square fa-fw" style="cursor: pointer;"></span> third<br>
+<span class="chcklst-box fa fa-square fa-fw" style="cursor: pointer;"></span> fourth</p>
 </div>`;
 
   const model = Post.create({ id: 42, can_edit: true });
-
-  const $elem = $(cooked);
   const decoratorHelper = { getModel: () => model };
+  const $elem = $(cooked);
 
   // eslint-disable-next-line no-undef
   server.get("/posts/42", () => [
@@ -69,7 +155,8 @@ Clicking the boxes will ruin the code block and the list becomes unresponsive.`;
 
   const done = assert.async();
   model.save = fields => {
-    assert.ok(fields.raw.indexOf("[ ] third") !== -1);
+    assert.ok(fields.raw.includes("[ ] third"));
+
     done();
   };

GitHub sha: bf0cf5d1