From 77801aa9be9268ddbc02dcb2473e466a196b9ed0 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX 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("

hello \":blonde_woman:t6:\"

") expect(PrettyText.cook("hello 👩‍🎤")).to eq("

hello \":woman_singer:\"

")