From 37886c28da55c0e62cc9dbb24fb14c02b3789d0c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 20 Apr 2023 16:43:55 +0200 Subject: [PATCH] Fix characters being emojified even when using Variation Selector 15 (text) (#20949) --- .../features/emoji/__tests__/emoji-test.js | 4 +- .../mastodon/features/emoji/emoji.js | 74 +++++++++++-------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 72a732e3bcf..7b917ac43b8 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -74,9 +74,9 @@ describe('emoji', () => { .toEqual('😇'); }); - it('skips the textual presentation VS15 character', () => { + it('does not emojify emojis with textual presentation VS15 character', () => { expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 - .toEqual('✴'); + .toEqual('✴︎'); }); it('does an simple emoji properly', () => { diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 6ae40662421..fe4f6f28695 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -20,13 +20,18 @@ const emojiFilename = (filename) => { }; const emojifyTextNode = (node, customEmojis) => { + const VS15 = 0xFE0E; + const VS16 = 0xFE0F; + let str = node.textContent; const fragment = new DocumentFragment(); + let i = 0; for (;;) { - let match, i = 0; + let match; + // Skip to the next potential emoji to replace if (customEmojis === null) { while (i < str.length && !(match = trie.search(str.slice(i)))) { i += str.codePointAt(i) < 65536 ? 1 : 2; @@ -37,51 +42,58 @@ const emojifyTextNode = (node, customEmojis) => { } } - let rend, replacement = null; + // We reached the end of the string, nothing to replace if (i === str.length) { break; - } else if (str[i] === ':') { - if (!(() => { - rend = str.indexOf(':', i + 1) + 1; - if (!rend) return false; // no pair of ':' - const shortname = str.slice(i, rend); - // now got a replacee as ':shortname:' - // if you want additional emoji handler, add statements below which set replacement and return true. - if (shortname in customEmojis) { - const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = document.createElement('img'); - replacement.setAttribute('draggable', 'false'); - replacement.setAttribute('class', 'emojione custom-emoji'); - replacement.setAttribute('alt', shortname); - replacement.setAttribute('title', shortname); - replacement.setAttribute('src', filename); - replacement.setAttribute('data-original', customEmojis[shortname].url); - replacement.setAttribute('data-static', customEmojis[shortname].static_url); - return true; - } - return false; - })()) rend = ++i; + } + + let rend, replacement = null; + if (str[i] === ':') { // Potentially the start of a custom emoji shortcode + if (!(rend = str.indexOf(':', i + 1) + 1)) { + continue; // no pair of ':' + } + + const shortname = str.slice(i, rend); + // now got a replacee as ':shortname:' + // if you want additional emoji handler, add statements below which set replacement and return true. + if (shortname in customEmojis) { + const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; + replacement = document.createElement('img'); + replacement.setAttribute('draggable', 'false'); + replacement.setAttribute('class', 'emojione custom-emoji'); + replacement.setAttribute('alt', shortname); + replacement.setAttribute('title', shortname); + replacement.setAttribute('src', filename); + replacement.setAttribute('data-original', customEmojis[shortname].url); + replacement.setAttribute('data-static', customEmojis[shortname].static_url); + } else { + continue; + } } else { // matched to unicode emoji + rend = i + match.length; + + // If the matched character was followed by VS15 (for selecting text presentation), skip it. + if (str.codePointAt(rend - 1) !== VS16 && str.codePointAt(rend) === VS15) { + i = rend + 1; + continue; + } + const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; + replacement = document.createElement('img'); replacement.setAttribute('draggable', 'false'); replacement.setAttribute('class', 'emojione'); replacement.setAttribute('alt', match); replacement.setAttribute('title', title); replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); - rend = i + match.length; - // If the matched character was followed by VS15 (for selecting text presentation), skip it. - if (str.codePointAt(rend) === 65038) { - rend += 1; - } } + // Add the processed-up-to-now string and the emoji replacement fragment.append(document.createTextNode(str.slice(0, i))); - if (replacement) { - fragment.append(replacement); - } + fragment.append(replacement); str = str.slice(rend); + i = 0; } fragment.append(document.createTextNode(str));