diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/javascript/mastodon/features/emoji/emoji.js | 80 | ||||
-rw-r--r-- | app/lib/emoji_formatter.rb | 68 |
2 files changed, 76 insertions, 72 deletions
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index fb1a3804c..0ab32767a 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -19,15 +19,23 @@ const emojiFilename = (filename) => { return borderedEmoji.includes(filename) ? (filename + '_border') : filename; }; -const emojify = (str, customEmojis = {}) => { - const tagCharsWithoutEmojis = '<&'; - const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; - let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; +const emojifyTextNode = (node, customEmojis) => { + const parentElement = node.parentElement; + let str = node.textContent; + for (;;) { - let match, i = 0, tag; - 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 match, i = 0; + + if (customEmojis === null) { + while (i < str.length && !(match = trie.search(str.slice(i)))) { + i += str.codePointAt(i) < 65536 ? 1 : 2; + } + } else { + while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) { + i += str.codePointAt(i) < 65536 ? 1 : 2; + } } + let rend, replacement = ''; if (i === str.length) { break; @@ -35,8 +43,6 @@ const emojify = (str, customEmojis = {}) => { if (!(() => { rend = str.indexOf(':', i + 1) + 1; if (!rend) return false; // no pair of ':' - const lt = str.indexOf('<', i + 1); - if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':' 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. @@ -47,29 +53,6 @@ const emojify = (str, customEmojis = {}) => { } return false; })()) rend = ++i; - } else if (tag >= 0) { // <, & - rend = str.indexOf('>;'[tag], i + 1) + 1; - 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]; const title = shortCode ? `:${shortCode}:` : ''; @@ -80,10 +63,39 @@ const emojify = (str, customEmojis = {}) => { rend += 1; } } - rtn += str.slice(0, i) + replacement; + + node.textContent = str.slice(0, i); + parentElement.insertAdjacentHTML('beforeend', replacement); str = str.slice(rend); + node = document.createTextNode(str); + parentElement.append(node); + } +}; + +const emojifyNode = (node, customEmojis) => { + for (const child of node.childNodes) { + switch(child.nodeType) { + case Node.TEXT_NODE: + emojifyTextNode(child, customEmojis); + break; + case Node.ELEMENT_NODE: + if (!child.classList.contains('invisible')) + emojifyNode(child, customEmojis); + break; + } } - return rtn + str; +}; + +const emojify = (str, customEmojis = {}) => { + const wrapper = document.createElement('div'); + wrapper.innerHTML = str; + + if (!Object.keys(customEmojis).length) + customEmojis = null; + + emojifyNode(wrapper, customEmojis); + + return wrapper.innerHTML; }; export default emojify; diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index 194849c23..a9785d5f9 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -23,48 +23,40 @@ class EmojiFormatter def to_s return html if custom_emojis.empty? || html.blank? - i = -1 - tag_open_index = nil - inside_shortname = false - shortname_start_index = -1 - invisible_depth = 0 - last_index = 0 - result = ''.dup - - while i + 1 < html.size - i += 1 - - if invisible_depth.zero? && inside_shortname && html[i] == ':' - inside_shortname = false - shortcode = html[shortname_start_index + 1..i - 1] - char_after = html[i + 1] - - next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode]) - - result << html[last_index..shortname_start_index - 1] if shortname_start_index.positive? - result << image_for_emoji(shortcode, emoji) - last_index = i + 1 - 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 + tree = Nokogiri::HTML.fragment(html) + tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node| + i = -1 + inside_shortname = false + shortname_start_index = -1 + last_index = 0 + text = node.content + result = Nokogiri::XML::NodeSet.new(tree.document) + + while i + 1 < text.size + i += 1 + + if inside_shortname && text[i] == ':' + inside_shortname = false + shortcode = text[shortname_start_index + 1..i - 1] + char_after = text[i + 1] + + next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode]) + + result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive? + result << Nokogiri::HTML.fragment(image_for_emoji(shortcode, emoji)) + + last_index = i + 1 + elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1])) + inside_shortname = true + shortname_start_index = i end - elsif html[i] == '<' - tag_open_index = i - inside_shortname = false - elsif !tag_open_index && html[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(html[i - 1])) - inside_shortname = true - shortname_start_index = i end - end - result << html[last_index..-1] + result << Nokogiri::XML::Text.new(text[last_index..-1], tree.document) + node.replace(result) + end - result.html_safe # rubocop:disable Rails/OutputSafety + tree.to_html.html_safe # rubocop:disable Rails/OutputSafety end private |