Repository: zestedesavoir/zmarkdown
Pull request #272 (2018-09-01 17:27:57)

remark-emoticons: fix leading spaces eating bug
A trimming issue led to the space in `*foo* :)` to be incorrectly eaten.


Patch:
diff --git a/packages/remark-emoticons/dist/index.js b/packages/remark-emoticons/dist/index.js
index 74d5e140..36ebe8f3 100644
--- a/packages/remark-emoticons/dist/index.js
+++ b/packages/remark-emoticons/dist/index.js
@@ -6,8 +6,6 @@ function escapeRegExp(str) {
   return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); // eslint-disable-line no-useless-escape
 }
 
-var SPACE = ' ';
-
 module.exports = function inlinePlugin(ctx) {
   var emoticonClasses = ctx && ctx.classes;
   var emoticons = ctx && ctx.emoticons;
@@ -48,11 +46,17 @@ module.exports = function inlinePlugin(ctx) {
 
   var pattern = Object.keys(emoticons).map(escapeRegExp).join('|');
 
-  var regex = new RegExp('(\\s|^)(' + pattern + ')(\\s|$)', 'i');
+  var regex = new RegExp('(?:\\s|^)(' + pattern + ')(?:\\s|$)', 'i');
 
   function locator(value, fromIndex) {
     var keep = regex.exec(value);
-    if (keep && value[keep.index] === SPACE) return keep.index + 1;
+    if (keep) {
+      var index = keep.index;
+      while (/^\s/.test(value.charAt(index))) {
+        index++;
+      }
+      return index;
+    }
     return -1;
   }
 
@@ -60,14 +64,12 @@ module.exports = function inlinePlugin(ctx) {
     var keep = regex.exec(value);
     if (keep) {
       if (keep.index !== 0) return true;
+      if (!keep[0].startsWith(keep[1])) return true;
 
       /* istanbul ignore if - never used (yet) */
       if (silent) return true;
 
-      var toEat = keep[0];
-      if (toEat.charAt(toEat.length - 1) === SPACE) {
-        toEat = toEat.substring(0, toEat.length - 1);
-      }
+      var toEat = keep[1];
       var emoticon = toEat.trim();
       var src = emoticons[emoticon.toLowerCase()];
 
diff --git a/packages/remark-emoticons/src/index.js b/packages/remark-emoticons/src/index.js
index 2eb48cfe..9938c1d0 100644
--- a/packages/remark-emoticons/src/index.js
+++ b/packages/remark-emoticons/src/index.js
@@ -2,8 +2,6 @@ function escapeRegExp (str) {
   return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') // eslint-disable-line no-useless-escape
 }
 
-const SPACE = ' '
-
 module.exports = function inlinePlugin (ctx) {
   const emoticonClasses = ctx && ctx.classes
   const emoticons = ctx && ctx.emoticons
@@ -18,11 +16,17 @@ module.exports = function inlinePlugin (ctx) {
 
   const pattern = Object.keys(emoticons).map(escapeRegExp).join('|')
 
-  const regex = new RegExp(`(\\s|^)(${pattern})(\\s|$)`, 'i')
+  const regex = new RegExp(`(?:\\s|^)(${pattern})(?:\\s|$)`, 'i')
 
   function locator (value, fromIndex) {
     const keep = regex.exec(value)
-    if (keep && value[keep.index] === SPACE) return keep.index + 1
+    if (keep) {
+      let index = keep.index
+      while (/^\s/.test(value.charAt(index))) {
+        index++
+      }
+      return index
+    }
     return -1
   }
 
@@ -30,14 +34,12 @@ module.exports = function inlinePlugin (ctx) {
     const keep = regex.exec(value)
     if (keep) {
       if (keep.index !== 0) return true
+      if (!keep[0].startsWith(keep[1])) return true
 
       /* istanbul ignore if - never used (yet) */
       if (silent) return true
 
-      let toEat = keep[0]
-      if (toEat.charAt(toEat.length - 1) === SPACE) {
-        toEat = toEat.substring(0, toEat.length - 1)
-      }
+      const toEat = keep[1]
       const emoticon = toEat.trim()
       const src = emoticons[emoticon.toLowerCase()]
 


Test patch:
diff --git a/packages/remark-emoticons/__tests__/__snapshots__/index.js.snap b/packages/remark-emoticons/__tests__/__snapshots__/index.js.snap
index 2d64d771..996b4188 100644
--- a/packages/remark-emoticons/__tests__/__snapshots__/index.js.snap
+++ b/packages/remark-emoticons/__tests__/__snapshots__/index.js.snap
@@ -11,7 +11,7 @@ exports[`emoticons 1`] = `
 <p><img src=\\"/static/smileys/heureux.png\\" alt=\\":D\\" class=\\"foo bar\\"> This is not a caption</p>
 <p>This is not a smiley:)</p>
 <p>Not :)a smiley either.</p>
-<p>Smiley after another node: <a href=\\"#foo\\">link</a><img src=\\"/static/smileys/smile.png\\" alt=\\":)\\" class=\\"foo bar\\"></p>
+<p>Smiley after another node: <a href=\\"#foo\\">link</a> <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\" class=\\"foo bar\\"></p>
 <p>Smiley after another node w/2 spaces: <a href=\\"#foo\\">link</a>  <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\" class=\\"foo bar\\"></p>
 <p>Smiley after another node w/3 spaces: <a href=\\"#foo\\">link</a>   <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\" class=\\"foo bar\\"></p>"
 `;
@@ -27,7 +27,7 @@ exports[`emoticons without class 1`] = `
 <p><img src=\\"/static/smileys/heureux.png\\" alt=\\":D\\"> This is not a caption</p>
 <p>This is not a smiley:)</p>
 <p>Not :)a smiley either.</p>
-<p>Smiley after another node: <a href=\\"#foo\\">link</a><img src=\\"/static/smileys/smile.png\\" alt=\\":)\\"></p>
+<p>Smiley after another node: <a href=\\"#foo\\">link</a> <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\"></p>
 <p>Smiley after another node w/2 spaces: <a href=\\"#foo\\">link</a>  <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\"></p>
 <p>Smiley after another node w/3 spaces: <a href=\\"#foo\\">link</a>   <img src=\\"/static/smileys/smile.png\\" alt=\\":)\\"></p>"
 `;
@@ -49,7 +49,7 @@ This is not a smiley:)
 
 Not :)a smiley either.
 
-Smiley after another node: [link](#foo):)
+Smiley after another node: [link](#foo) :)
 
 Smiley after another node w/2 spaces: [link](#foo)  :)
 
diff --git a/packages/remark-emoticons/__tests__/index.js b/packages/remark-emoticons/__tests__/index.js
index af4d6c29..47ccf0ea 100644
--- a/packages/remark-emoticons/__tests__/index.js
+++ b/packages/remark-emoticons/__tests__/index.js
@@ -80,6 +80,16 @@ test('emoticons', () => {
   expect(contents).toMatchSnapshot()
 })
 
+test('does not eat spaces leading to a smiley', () => {
+  const {contents} = render(dedent`
+    *hey* :)
+
+    [ho](#) :D let's go
+  `)
+  expect(contents).not.toContain('</em><img')
+  expect(contents).not.toContain('</a><img')
+})
+
 test('emoticons without class', () => {
   const render = text => unified()
     .use(reParse)