From 95e5f8380d09542a57a99de8ca94596561b1b9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 4 Jan 2019 15:14:16 +0100 Subject: [PATCH] FEATURE: Allow plugins to add custom emoji translations FIX: buildTranslationTree was erroring when translations overlapped (ie. ":-)" and ":-))") FIX: emoji translations wasn't working properly when translations overlapped --- .../pretty-text/emoji/data.js.es6.erb | 31 +--------- .../engines/discourse-markdown/emoji.js.es6 | 58 +++++++----------- app/models/emoji.rb | 60 +++++++++++-------- lib/emoji/db.json | 30 +++++++++- lib/plugin/instance.rb | 13 ++++ 5 files changed, 98 insertions(+), 94 deletions(-) diff --git a/app/assets/javascripts/pretty-text/emoji/data.js.es6.erb b/app/assets/javascripts/pretty-text/emoji/data.js.es6.erb index 765ecc3303e..32a9b986bdc 100644 --- a/app/assets/javascripts/pretty-text/emoji/data.js.es6.erb +++ b/app/assets/javascripts/pretty-text/emoji/data.js.es6.erb @@ -1,32 +1,5 @@ export const emojis = <%= Emoji.standard.map(&:name).flatten.inspect %>; export const tonableEmojis = <%= Emoji.tonable_emojis.flatten.inspect %>; export const aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>; -export const searchAliases = <%= Emoji.searchAliases.inspect.gsub("=>", ":") %>; -export const translations = { - ':)' : 'slight_smile', - ':-)' : 'slight_smile', - '^_^' : 'slight_smile', - '^__^' : 'slight_smile', - ':(' : 'frowning', - ':-(' : 'frowning', - ';)' : 'wink', - ';-)' : 'wink', - ':\'(' : 'cry', - ':\'-(': 'cry', - ':-\'(': 'cry', - ':p' : 'stuck_out_tongue', - ':P' : 'stuck_out_tongue', - ':-P' : 'stuck_out_tongue', - ':O' : 'open_mouth', - ':-O' : 'open_mouth', - ':D' : 'smiley', - ':-D' : 'smiley', - ':|' : 'expressionless', - ':-|' : 'expressionless', - ':/' : 'confused', - '8-)' : 'sunglasses', - ";P" : 'stuck_out_tongue_winking_eye', - ";-P" : 'stuck_out_tongue_winking_eye', - ":$" : 'blush', - ":-$" : 'blush' -}; +export const searchAliases = <%= Emoji.search_aliases.inspect.gsub("=>", ":") %>; +export const translations = <%= Emoji.translations.inspect.gsub("=>", ":") %>; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 index 548cf74638f..ad9e83414ff 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 @@ -9,22 +9,18 @@ let translationTree = null; // We build a data structure that allows us to quickly // search through our N next chars to see if any match // one of our alias emojis. -// function buildTranslationTree() { let tree = []; let lastNode; - Object.keys(translations).forEach(function(key) { - let i; + Object.keys(translations).forEach(key => { let node = tree; - for (i = 0; i < key.length; i++) { + for (let i = 0; i < key.length; i++) { let code = key.charCodeAt(i); - let j; - let found = false; - for (j = 0; j < node.length; j++) { + for (let j = 0; j < node.length; j++) { if (node[j][0] === code) { node = node[j][1]; found = true; @@ -33,7 +29,7 @@ function buildTranslationTree() { } if (!found) { - // token, children, value + // code, children, value let tmp = [code, []]; node.push(tmp); lastNode = tmp; @@ -41,7 +37,7 @@ function buildTranslationTree() { } } - lastNode[1] = translations[key]; + lastNode[2] = translations[key]; }); return tree; @@ -121,28 +117,22 @@ function getEmojiTokenByName(name, state) { function getEmojiTokenByTranslation(content, pos, state) { translationTree = translationTree || buildTranslationTree(); - let currentTree = translationTree; - - let i; - let search = true; - let found = false; + let t = translationTree; let start = pos; + let found = null; - while (search) { - search = false; + while (t.length > 0 && pos < content.length) { let code = content.charCodeAt(pos); - for (i = 0; i < currentTree.length; i++) { - if (currentTree[i][0] === code) { - currentTree = currentTree[i][1]; - pos++; - search = true; - if (typeof currentTree === "string") { - found = currentTree; - } + for (let i = 0; i < t.length; i++) { + if (t[i][0] === code) { + found = t[i][2]; + t = t[i][1]; break; } } + + pos++; } if (!found) { @@ -174,17 +164,9 @@ function getEmojiTokenByTranslation(content, pos, state) { } } -function applyEmoji( - content, - state, - emojiUnicodeReplacer, - enableShortcuts, - inlineEmoji -) { - let i; +function applyEmoji(content, state, emojiUnicodeReplacer, enableShortcuts, inlineEmoji) { let result = null; let contentToken = null; - let start = 0; if (emojiUnicodeReplacer) { @@ -193,10 +175,10 @@ function applyEmoji( let endToken = content.length; - for (i = 0; i < content.length - 1; i++) { + for (let i = 0; i < content.length - 1; i++) { let offset = 0; - const emojiName = getEmojiName(content, i, state, inlineEmoji); let token = null; + const emojiName = getEmojiName(content, i, state, inlineEmoji); if (emojiName) { token = getEmojiTokenByName(emojiName, state); @@ -207,8 +189,7 @@ function applyEmoji( if (enableShortcuts && !token) { // handle aliases (note: we can't do this in inline cause ; is not a split point) - // - let info = getEmojiTokenByTranslation(content, i, state); + const info = getEmojiTokenByTranslation(content, i, state); if (info) { offset = info.pos - i; @@ -225,7 +206,8 @@ function applyEmoji( } result.push(token); - endToken = start = i + offset; + i += offset; + endToken = start = i; } } diff --git a/app/models/emoji.rb b/app/models/emoji.rb index e7889896ef4..c20d26ef061 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -25,10 +25,14 @@ class Emoji Discourse.cache.fetch(cache_key("aliases_emojis")) { db['aliases'] } end - def self.searchAliases + def self.search_aliases Discourse.cache.fetch(cache_key("search_aliases_emojis")) { db['searchAliases'] } end + def self.translations + Discourse.cache.fetch(cache_key("translations_emojis")) { load_translations } + end + def self.custom Discourse.cache.fetch(cache_key("custom_emojis")) { load_custom } end @@ -63,13 +67,13 @@ class Emoji end def self.clear_cache - %w{custom standard aliases search_aliases all tonable}.each do |key| + %w{custom standard aliases search_aliases translations all tonable}.each do |key| Discourse.cache.delete(cache_key("#{key}_emojis")) end end def self.db_file - "#{Rails.root}/lib/emoji/db.json" + @db_file ||= "#{Rails.root}/lib/emoji/db.json" end def self.db @@ -101,6 +105,10 @@ class Emoji result end + def self.load_translations + db["translations"].merge(Plugin::CustomEmoji.translations) + end + def self.base_directory "public#{base_url}" end @@ -117,35 +125,35 @@ class Emoji end def self.unicode_replacements - return @unicode_replacements if @unicode_replacements + @unicode_replacements ||= begin + replacements = {} + is_tonable_emojis = Emoji.tonable_emojis + fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) } - @unicode_replacements = {} - is_tonable_emojis = Emoji.tonable_emojis - fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) } + db['emojis'].each do |e| + name = e['name'] + next if name == 'tm'.freeze - db['emojis'].each do |e| - name = e['name'] - next if name == 'tm'.freeze + code = replacement_code(e['code']) + next unless code - code = replacement_code(e['code']) - next unless code - - @unicode_replacements[code] = name - if is_tonable_emojis.include?(name) - fitzpatrick_scales.each_with_index do |scale, index| - toned_code = code.codepoints.insert(1, scale).pack("U*".freeze) - @unicode_replacements[toned_code] = "#{name}:t#{index + 2}" + replacements[code] = name + if is_tonable_emojis.include?(name) + fitzpatrick_scales.each_with_index do |scale, index| + toned_code = code.codepoints.insert(1, scale).pack("U*".freeze) + replacements[toned_code] = "#{name}:t#{index + 2}" + end end end + + replacements["\u{2639}"] = 'frowning' + replacements["\u{263A}"] = 'slight_smile' + replacements["\u{263B}"] = 'slight_smile' + replacements["\u{2661}"] = 'heart' + replacements["\u{2665}"] = 'heart' + + replacements end - - @unicode_replacements["\u{2639}"] = 'frowning' - @unicode_replacements["\u{263A}"] = 'slight_smile' - @unicode_replacements["\u{263B}"] = 'slight_smile' - @unicode_replacements["\u{2661}"] = 'heart' - @unicode_replacements["\u{2665}"] = 'heart' - - @unicode_replacements end def self.unicode_unescape(string) diff --git a/lib/emoji/db.json b/lib/emoji/db.json index 31bf152c419..90c09f070d5 100644 --- a/lib/emoji/db.json +++ b/lib/emoji/db.json @@ -7115,5 +7115,33 @@ "cry": [ "sob" ] + }, + "translations": { + ":)" : "slight_smile", + ":-)" : "slight_smile", + "^_^" : "slight_smile", + "^__^": "slight_smile", + ":(" : "frowning", + ":-(" : "frowning", + ";)" : "wink", + ";-)" : "wink", + ":'(" : "cry", + ":'-(": "cry", + ":-'(": "cry", + ":p" : "stuck_out_tongue", + ":P" : "stuck_out_tongue", + ":-P" : "stuck_out_tongue", + ":O" : "open_mouth", + ":-O" : "open_mouth", + ":D" : "smiley", + ":-D" : "smiley", + ":|" : "expressionless", + ":-|" : "expressionless", + ":/" : "confused", + "8-)" : "sunglasses", + ";P" : "stuck_out_tongue_winking_eye", + ";-P" : "stuck_out_tongue_winking_eye", + ":$" : "blush", + ":-$" : "blush" } -} \ No newline at end of file +} diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index b19d6cedb96..53552d0b479 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -16,6 +16,15 @@ class Plugin::CustomEmoji @@cache_key = Digest::SHA1.hexdigest(cache_key + name)[0..10] emojis[name] = url end + + def self.translations + @@translations ||= {} + end + + def self.translate(from, to) + @@cache_key = Digest::SHA1.hexdigest(cache_key + from)[0..10] + translations[from] = to + end end class Plugin::Instance @@ -429,6 +438,10 @@ class Plugin::Instance Plugin::CustomEmoji.register(name, url) end + def translate_emoji(from, to) + Plugin::CustomEmoji.translate(from, to) + end + def automatic_assets css = styles.join("\n") js = javascripts.join("\n")