FIX: Ignore checkboxes inside code blocks. (#16)

FIX: Ignore checkboxes inside code blocks. (#16)

diff --git a/assets/javascripts/discourse/initializers/checklist.js.es6 b/assets/javascripts/discourse/initializers/checklist.js.es6
index 9befebb..e073da0 100644
--- a/assets/javascripts/discourse/initializers/checklist.js.es6
+++ b/assets/javascripts/discourse/initializers/checklist.js.es6
@@ -1,6 +1,6 @@
 import { withPluginApi } from "discourse/lib/plugin-api";
-import AjaxLib from "discourse/lib/ajax";
-import TextLib from "discourse/lib/text";
+import { ajax } from "discourse/lib/ajax";
+import { cookAsync } from "discourse/lib/text";
 import { iconHTML } from "discourse-common/lib/icon-library";
 
 function initializePlugin(api) {
@@ -30,33 +30,42 @@ export function checklistSyntax($elem, post) {
 
       $box.after(iconHTML("spinner", { class: "fa-spin" })).hide();
 
-      const endpoint = Discourse.getURL(`/posts/${postModel.id}`);
-      AjaxLib.ajax(endpoint, { type: "GET", cache: false }).then(result => {
-        // make the first run go to index = 0
-        let nth = -1;
-        const newRaw = result.raw.replace(
-          /\[(\s|\_|\-|\x|\\?\*)?\]/gi,
-          match => {
-            nth += 1;
-            return nth === idx ? newValue : match;
-          }
-        );
+      ajax(`/posts/${postModel.id}`, { type: "GET", cache: false }).then(
+        result => {
+          const blocks = [];
 
-        const props = {
-          raw: newRaw,
-          edit_reason: I18n.t("checklist.edit_reason")
-        };
-
-        if (TextLib.cookAsync) {
-          TextLib.cookAsync(newRaw).then(cooked => {
-            props.cooked = cooked.string;
-            postModel.save(props);
+          // Computing offsets where checkbox are not evaluated (i.e. inside
+          // code blocks).
+          [
+            /`[^`\n]*\n?[^`\n]*`/gm,
+            /^`‍``.*?^`‍``/gms,
+            /\[code\].*?\[\/code\]/gms
+          ].forEach(regex => {
+            let match;
+            while ((match = regex.exec(result.raw)) != null) {
+              blocks.push([match.index, match.index + match[0].length]);
+            }
           });
-        } else {
-          props.cooked = TextLib.cook(newRaw).string;
-          postModel.save(props);
+
+          // make the first run go to index = 0
+          let nth = -1;
+          const newRaw = result.raw.replace(
+            /\[(\s|\_|\-|\x|\\?\*)?\]/gi,
+            (match, ignored, off) => {
+              nth += blocks.every(b => b[0] > off + match.length || off > b[1]);
+              return nth === idx ? newValue : match;
+            }
+          );
+
+          cookAsync(newRaw).then(cooked =>
+            postModel.save({
+              cooked: cooked.string,
+              raw: newRaw,
+              edit_reason: I18n.t("checklist.edit_reason")
+            })
+          );
         }
-      });
+      );
     });
   });
 
diff --git a/test/javascripts/lib/checklist-test.js.es6 b/test/javascripts/lib/checklist-test.js.es6
new file mode 100644
index 0000000..b927861
--- /dev/null
+++ b/test/javascripts/lib/checklist-test.js.es6
@@ -0,0 +1,62 @@
+import Post from "discourse/models/post";
+import { checklistSyntax } from "discourse/plugins/discourse-checklist/discourse/initializers/checklist";
+
+QUnit.module("initializer:checklist");
+
+QUnit.test("correct checkbox is selected", assert => {
+  const raw = `Hi there,
+
+It seems that a code block followed by a checklist breaks things.
+
+\`\`\`
+[\*]
+[ ]
+[ ]
+[\*]
+\`\`\`
+Will create a list like this:
+[] 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><code>[\*]
+[ ]
+[ ]
+[\*]
+</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>
+</div>`;
+
+  const model = Post.create({ id: 42, can_edit: true });
+
+  const $elem = $(cooked);
+  const decoratorHelper = { getModel: () => model };
+
+  // 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.indexOf("[ ] third") !== -1);
+    done();
+  };
+
+  $elem.find(".chcklst-box:nth(2)").click();
+});

GitHub sha: a0a57b27

1 Like