@@ -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" />'); | |||
}); | |||
}); | |||
}); |
@@ -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]; | |||
@@ -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 | |||