about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji-test.js16
-rw-r--r--app/javascript/mastodon/features/emoji/emoji.js27
-rw-r--r--app/lib/formatter.rb26
3 files changed, 60 insertions, 9 deletions
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 636402172..372459c78 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -57,5 +57,21 @@ describe('emoji', () => {
     it('does an emoji whose filename is irregular', () => {
       expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
     });
+
+    it('avoid emojifying on invisible text', () => {
+      expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
+        .toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
+      expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
+        .toEqual('<span class="invisible">:luigi:</span>');
+    });
+
+    it('avoid emojifying on invisible text with nested tags', () => {
+      expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+      expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+      expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+    });
   });
 });
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index bda33ca26..0f005dd50 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -7,10 +7,12 @@ const trie = new Trie(Object.keys(unicodeMapping));
 const assetHost = process.env.CDN_HOST || '';
 
 const emojify = (str, customEmojis = {}) => {
-  let rtn = '';
+  const tagCharsWithoutEmojis = '<&';
+  const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
+  let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
   for (;;) {
     let match, i = 0, tag;
-    while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
+    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
       i += str.codePointAt(i) < 65536 ? 1 : 2;
     }
     let rend, replacement = '';
@@ -34,7 +36,26 @@ const emojify = (str, customEmojis = {}) => {
       })()) rend = ++i;
     } else if (tag >= 0) { // <, &
       rend = str.indexOf('>;'[tag], i + 1) + 1;
-      if (!rend) break;
+      if (!rend) {
+        break;
+      }
+      if (tag === 0) {
+        if (invisible) {
+          if (str[i + 1] === '/') { // closing tag
+            if (!--invisible) {
+              tagChars = tagCharsWithEmojis;
+            }
+          } else if (str[rend - 2] !== '/') { // opening tag
+            invisible++;
+          }
+        } else {
+          if (str.startsWith('<span class="invisible">', i)) {
+            // avoid emojifying on invisible text
+            invisible = 1;
+            tagChars = tagCharsWithoutEmojis;
+          }
+        }
+      }
       i = rend;
     } else { // matched to unicode emoji
       const { filename, shortCode } = unicodeMapping[match];
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 57f105da7..733a1c4b7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -89,20 +89,28 @@ class Formatter
     end
   end
 
+  def count_tag_nesting(tag)
+    if tag[1] == '/' then -1
+    elsif tag[-2] == '/' then 0
+    else 1
+    end
+  end
+
   def encode_custom_emojis(html, emojis)
     return html if emojis.empty?
 
     emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
 
     i                     = -1
-    inside_tag            = false
+    tag_open_index        = nil
     inside_shortname      = false
     shortname_start_index = -1
+    invisible_depth       = 0
 
     while i + 1 < html.size
       i += 1
 
-      if inside_shortname && html[i] == ':'
+      if invisible_depth.zero? && inside_shortname && html[i] == ':'
         shortcode = html[shortname_start_index + 1..i - 1]
         emoji     = emoji_map[shortcode]
 
@@ -116,12 +124,18 @@ class Formatter
         end
 
         inside_shortname = false
-      elsif inside_tag && html[i] == '>'
-        inside_tag = false
+      elsif tag_open_index && html[i] == '>'
+        tag = html[tag_open_index..i]
+        tag_open_index = nil
+        if invisible_depth.positive?
+          invisible_depth += count_tag_nesting(tag)
+        elsif tag == '<span class="invisible">'
+          invisible_depth = 1
+        end
       elsif html[i] == '<'
-        inside_tag       = true
+        tag_open_index   = i
         inside_shortname = false
-      elsif !inside_tag && html[i] == ':'
+      elsif !tag_open_index && html[i] == ':'
         inside_shortname      = true
         shortname_start_index = i
       end