commit: 782224c99151665470b138add583d50df17b4380
parent: 84cfee2488ed0d795b69ffe51e7260548c2d6af3
Author: MIYAGI Hikaru <hcmiya@users.noreply.github.com>
Date: Tue, 7 Nov 2017 22:48:13 +0900
Avoid emojifying on invisible text (#5558)
Diffstat:
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
@@ -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
@@ -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
@@ -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