From 77801aa9be9268ddbc02dcb2473e466a196b9ed0 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Date: Wed, 27 May 2020 20:11:52 +0200
Subject: [PATCH] FIX: allows to have custom emoji translation without static
 file (#9893)

---
 .../discourse/app/components/d-editor.js      | 11 +++++-
 .../javascripts/discourse/app/lib/text.js     |  1 +
 .../javascripts/pretty-text/addon/emoji.js    | 18 +++++++--
 .../addon/engines/discourse-markdown/emoji.js | 39 +++++++++++++++----
 .../pretty-text/addon/pretty-text.js          |  4 +-
 app/models/emoji.rb                           |  2 +-
 app/serializers/site_serializer.rb            |  7 +++-
 lib/plugin/instance.rb                        |  1 +
 lib/pretty_text.rb                            |  5 +++
 lib/pretty_text/shims.js                      |  2 +
 spec/components/pretty_text_spec.rb           | 21 ++++++++++
 11 files changed, 94 insertions(+), 17 deletions(-)

diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js
index bfdf021bff9..184b7f93a6e 100644
--- a/app/assets/javascripts/discourse/app/components/d-editor.js
+++ b/app/assets/javascripts/discourse/app/components/d-editor.js
@@ -483,8 +483,15 @@ export default Component.extend({
             }
           }
 
-          if (translations[full]) {
-            return resolve([translations[full]]);
+          // note this will only work for emojis starting with :
+          // eg: :-)
+          const allTranslations = Object.assign(
+            {},
+            translations,
+            this.getWithDefault("site.custom_emoji_translation", {})
+          );
+          if (allTranslations[full]) {
+            return resolve([allTranslations[full]]);
           }
 
           const match = term.match(/^:?(.*?):t([2-6])?$/);
diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js
index 692e75b9b5d..94d33b18d57 100644
--- a/app/assets/javascripts/discourse/app/lib/text.js
+++ b/app/assets/javascripts/discourse/app/lib/text.js
@@ -18,6 +18,7 @@ function getOpts(opts) {
       getURL: getURLWithCDN,
       currentUser: Discourse.__container__.lookup("current-user:main"),
       censoredRegexp: site.censored_regexp,
+      customEmojiTranslation: site.custom_emoji_translation,
       siteSettings,
       formatUsername
     },
diff --git a/app/assets/javascripts/pretty-text/addon/emoji.js b/app/assets/javascripts/pretty-text/addon/emoji.js
index 74af3b90cdc..5d9fb762c01 100644
--- a/app/assets/javascripts/pretty-text/addon/emoji.js
+++ b/app/assets/javascripts/pretty-text/addon/emoji.js
@@ -98,13 +98,18 @@ export function performEmojiUnescape(string, opts) {
 
   const inlineEmoji = opts.inlineEmoji;
   const regexp = unicodeRegexp(inlineEmoji);
+  const allTranslations = Object.assign(
+    {},
+    translations,
+    opts.customEmojiTranslation || {}
+  );
 
   return string.replace(regexp, (m, index) => {
-    const isEmoticon = opts.enableEmojiShortcuts && !!translations[m];
+    const isEmoticon = opts.enableEmojiShortcuts && !!allTranslations[m];
     const isUnicodeEmoticon = !!replacements[m];
     let emojiVal;
     if (isEmoticon) {
-      emojiVal = translations[m];
+      emojiVal = allTranslations[m];
     } else if (isUnicodeEmoticon) {
       emojiVal = replacements[m];
     } else {
@@ -131,11 +136,16 @@ export function performEmojiUnescape(string, opts) {
 export function performEmojiEscape(string, opts) {
   const inlineEmoji = opts.inlineEmoji;
   const regexp = unicodeRegexp(inlineEmoji);
+  const allTranslations = Object.assign(
+    {},
+    translations,
+    opts.customEmojiTranslation || {}
+  );
 
   return string.replace(regexp, (m, index) => {
     if (isReplacableInlineEmoji(string, index, inlineEmoji)) {
-      if (!!translations[m]) {
-        return opts.emojiShortcuts ? `:${translations[m]}:` : m;
+      if (!!allTranslations[m]) {
+        return opts.emojiShortcuts ? `:${allTranslations[m]}:` : m;
       } else if (!!replacements[m]) {
         return `:${replacements[m]}:`;
       }
diff --git a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown/emoji.js b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown/emoji.js
index 7b4bee783d5..452bb5046da 100644
--- a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown/emoji.js
+++ b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown/emoji.js
@@ -5,15 +5,25 @@ const MAX_NAME_LENGTH = 60;
 
 let translationTree = null;
 
+export function resetTranslationTree() {
+  translationTree = null;
+}
+
 // This allows us to efficiently search for aliases
 // 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() {
+function buildTranslationTree(customEmojiTranslation) {
   let tree = [];
   let lastNode;
 
-  Object.keys(translations).forEach(key => {
+  const allTranslations = Object.assign(
+    {},
+    translations,
+    customEmojiTranslation || {}
+  );
+
+  Object.keys(allTranslations).forEach(key => {
     let node = tree;
 
     for (let i = 0; i < key.length; i++) {
@@ -37,7 +47,7 @@ function buildTranslationTree() {
       }
     }
 
-    lastNode[2] = translations[key];
+    lastNode[2] = allTranslations[key];
   });
 
   return tree;
@@ -114,8 +124,14 @@ function getEmojiTokenByName(name, state) {
   }
 }
 
-function getEmojiTokenByTranslation(content, pos, state) {
-  translationTree = translationTree || buildTranslationTree();
+function getEmojiTokenByTranslation(
+  content,
+  pos,
+  state,
+  customEmojiTranslation
+) {
+  translationTree =
+    translationTree || buildTranslationTree(customEmojiTranslation);
 
   let t = translationTree;
   let start = pos;
@@ -175,7 +191,8 @@ function applyEmoji(
   state,
   emojiUnicodeReplacer,
   enableShortcuts,
-  inlineEmoji
+  inlineEmoji,
+  customEmojiTranslation
 ) {
   let result = null;
   let start = 0;
@@ -201,7 +218,12 @@ function applyEmoji(
 
     if (enableShortcuts && !token) {
       // handle aliases (note: we can't do this in inline cause ; is not a split point)
-      const info = getEmojiTokenByTranslation(content, i, state);
+      const info = getEmojiTokenByTranslation(
+        content,
+        i,
+        state,
+        customEmojiTranslation
+      );
 
       if (info) {
         offset = info.pos - i;
@@ -310,7 +332,8 @@ export function setup(helper) {
           s,
           md.options.discourse.emojiUnicodeReplacer,
           md.options.discourse.features.emojiShortcuts,
-          md.options.discourse.features.inlineEmoji
+          md.options.discourse.features.inlineEmoji,
+          md.options.discourse.customEmojiTranslation
         )
       )
     );
diff --git a/app/assets/javascripts/pretty-text/addon/pretty-text.js b/app/assets/javascripts/pretty-text/addon/pretty-text.js
index 7a867d89c96..d24fb4af6f0 100644
--- a/app/assets/javascripts/pretty-text/addon/pretty-text.js
+++ b/app/assets/javascripts/pretty-text/addon/pretty-text.js
@@ -30,7 +30,8 @@ export function buildOptions(state) {
     previewing,
     linkify,
     censoredRegexp,
-    disableEmojis
+    disableEmojis,
+    customEmojiTranslation
   } = state;
 
   let features = {
@@ -68,6 +69,7 @@ export function buildOptions(state) {
     emojiUnicodeReplacer,
     lookupUploadUrls,
     censoredRegexp,
+    customEmojiTranslation,
     allowedHrefSchemes: siteSettings.allowed_href_schemes
       ? siteSettings.allowed_href_schemes.split("|")
       : null,
diff --git a/app/models/emoji.rb b/app/models/emoji.rb
index cfd121b76ec..7222cafebc2 100644
--- a/app/models/emoji.rb
+++ b/app/models/emoji.rb
@@ -126,7 +126,7 @@ class Emoji
   end
 
   def self.load_translations
-    db["translations"].merge(Plugin::CustomEmoji.translations)
+    db["translations"]
   end
 
   def self.base_directory
diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb
index 06acfc0c4f5..d5bb9013460 100644
--- a/app/serializers/site_serializer.rb
+++ b/app/serializers/site_serializer.rb
@@ -26,7 +26,8 @@ class SiteSerializer < ApplicationSerializer
     :topic_featured_link_allowed_category_ids,
     :user_themes,
     :censored_regexp,
-    :shared_drafts_category_id
+    :shared_drafts_category_id,
+    :custom_emoji_translation
   )
 
   has_many :categories, serializer: SiteCategorySerializer, embed: :objects
@@ -154,6 +155,10 @@ class SiteSerializer < ApplicationSerializer
     WordWatcher.word_matcher_regexp(:censor)&.source
   end
 
+  def custom_emoji_translation
+    Plugin::CustomEmoji.translations
+  end
+
   def shared_drafts_category_id
     SiteSetting.shared_drafts_category.to_i
   end
diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb
index 7d48dbbdde8..11f866f937c 100644
--- a/lib/plugin/instance.rb
+++ b/lib/plugin/instance.rb
@@ -18,6 +18,7 @@ class Plugin::CustomEmoji
   def self.clear_cache
     @@cache_key = CACHE_KEY
     @@emojis = {}
+    @@translations = {}
   end
 
   def self.register(name, url, group = Emoji::DEFAULT_GROUP)
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 0f114c51694..642b627c3df 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -126,6 +126,10 @@ module PrettyText
     @ctx
   end
 
+  def self.reset_translations
+    v8.eval("__resetTranslationTree()")
+  end
+
   def self.reset_context
     @ctx_init.synchronize do
       @ctx&.dispose
@@ -159,6 +163,7 @@ module PrettyText
         __optInput.getTopicInfo = __getTopicInfo;
         __optInput.categoryHashtagLookup = __categoryLookup;
         __optInput.customEmoji = #{custom_emoji.to_json};
+        __optInput.customEmojiTranslation = #{Plugin::CustomEmoji.translations.to_json};
         __optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
         __optInput.lookupUploadUrls = __lookupUploadUrls;
         __optInput.censoredRegexp = #{WordWatcher.word_matcher_regexp(:censor)&.source.to_json};
diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js
index cc288ceb40a..8e84568ed4d 100644
--- a/lib/pretty_text/shims.js
+++ b/lib/pretty_text/shims.js
@@ -3,6 +3,8 @@ __buildOptions = require("pretty-text/pretty-text").buildOptions;
 __performEmojiUnescape = require("pretty-text/emoji").performEmojiUnescape;
 __buildReplacementsList = require("pretty-text/emoji").buildReplacementsList;
 __performEmojiEscape = require("pretty-text/emoji").performEmojiEscape;
+__resetTranslationTree = require("pretty-text/engines/discourse-markdown/emoji")
+  .resetTranslationTree;
 
 I18n = {
   t(a, b) {
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index c5ef73c4d23..49a3ac90baa 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -1040,6 +1040,27 @@ describe PrettyText do
     end
   end
 
+  describe "custom emoji translation" do
+    before do
+      PrettyText.reset_translations
+
+      SiteSetting.enable_emoji = true
+      SiteSetting.enable_emoji_shortcuts = true
+
+      plugin = Plugin::Instance.new
+      plugin.translate_emoji "0:)", "otter"
+    end
+
+    after do
+      Plugin::CustomEmoji.clear_cache
+      PrettyText.reset_translations
+    end
+
+    it "sets the custom translation" do
+      expect(PrettyText.cook("hello 0:)")).to match(/otter/)
+    end
+  end
+
   it "replaces skin toned emoji" do
     expect(PrettyText.cook("hello 👱🏿‍♀️")).to eq("<p>hello <img src=\"/images/emoji/twitter/blonde_woman/6.png?v=#{Emoji::EMOJI_VERSION}\" title=\":blonde_woman:t6:\" class=\"emoji\" alt=\":blonde_woman:t6:\"></p>")
     expect(PrettyText.cook("hello 👩‍🎤")).to eq("<p>hello <img src=\"/images/emoji/twitter/woman_singer.png?v=#{Emoji::EMOJI_VERSION}\" title=\":woman_singer:\" class=\"emoji\" alt=\":woman_singer:\"></p>")