FEATURE: Allow plugins to add custom emoji translations

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

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 765ecc3..32a9b98 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 548cf74..ad9e834 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 e788989..c20d26e 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 = {}
-    is_tonable_emojis = Emoji.tonable_emojis
-    fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) }
+    @unicode_replacements ||= begin
+      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
-    end
 

[... diff too long, it was truncated ...]

GitHub
sha: 95e5f838