diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6
index 2767426c09e..7862ce53e01 100644
--- a/app/assets/javascripts/discourse/lib/text.js.es6
+++ b/app/assets/javascripts/discourse/lib/text.js.es6
@@ -68,7 +68,8 @@ function emojiOptions() {
return {
getURL: Discourse.getURLWithCDN,
emojiSet: Discourse.SiteSettings.emoji_set,
- enableEmojiShortcuts: Discourse.SiteSettings.enable_emoji_shortcuts
+ enableEmojiShortcuts: Discourse.SiteSettings.enable_emoji_shortcuts,
+ inlineEmoji: Discourse.SiteSettings.enable_inline_emoji_translation
};
}
diff --git a/app/assets/javascripts/pretty-text/emoji.js.es6 b/app/assets/javascripts/pretty-text/emoji.js.es6
index 2089902c3b5..fec9b725c4d 100644
--- a/app/assets/javascripts/pretty-text/emoji.js.es6
+++ b/app/assets/javascripts/pretty-text/emoji.js.es6
@@ -42,10 +42,31 @@ export function buildReplacementsList(emojiReplacements) {
.join("|");
}
-const unicodeRegexp = new RegExp(
- buildReplacementsList(replacements) + "|\\B:[^\\s:]+(?::t\\d)?:?\\B",
- "g"
-);
+let replacementListCache;
+const unicodeRegexpCache = {};
+
+function replacementList() {
+ if (replacementListCache === undefined) {
+ replacementListCache = buildReplacementsList(replacements);
+ }
+
+ return replacementListCache;
+}
+
+function unicodeRegexp(inlineEmoji) {
+ if (unicodeRegexpCache[inlineEmoji] === undefined) {
+ const emojiExpression = inlineEmoji
+ ? "|:[^\\s:]+(?::t\\d)?:?"
+ : "|\\B:[^\\s:]+(?::t\\d)?:?\\B";
+
+ unicodeRegexpCache[inlineEmoji] = new RegExp(
+ replacementList() + emojiExpression,
+ "g"
+ );
+ }
+
+ return unicodeRegexpCache[inlineEmoji];
+}
// add all default emojis
emojis.forEach(code => (emojiHash[code] = true));
@@ -56,12 +77,29 @@ Object.keys(aliases).forEach(name => {
aliases[name].forEach(alias => (aliasHash[alias] = name));
});
+function isReplacableInlineEmoji(string, index, inlineEmoji) {
+ if (inlineEmoji) return true;
+
+ // index depends on regex; when `inlineEmoji` is false, the regex starts
+ // with a `\B` character, so there's no need to subtract from the index
+ const beforeEmoji = string.slice(0, index - (inlineEmoji ? 1 : 0));
+
+ return (
+ beforeEmoji.length === 0 ||
+ /(?:\s|[>.,\/#!$%^&*;:{}=\-_`~()])$/.test(beforeEmoji) ||
+ new RegExp(`(?:${replacementList()})$`).test(beforeEmoji)
+ );
+}
+
export function performEmojiUnescape(string, opts) {
if (!string) {
return;
}
- return string.replace(unicodeRegexp, m => {
+ const inlineEmoji = opts.inlineEmoji;
+ const regexp = unicodeRegexp(inlineEmoji);
+
+ return string.replace(regexp, (m, index) => {
const isEmoticon = opts.enableEmojiShortcuts && !!translations[m];
const isUnicodeEmoticon = !!replacements[m];
let emojiVal;
@@ -78,7 +116,11 @@ export function performEmojiUnescape(string, opts) {
? "emoji emoji-custom"
: "emoji";
- return url && (isEmoticon || hasEndingColon || isUnicodeEmoticon)
+ const isReplacable =
+ (isEmoticon || hasEndingColon || isUnicodeEmoticon) &&
+ isReplacableInlineEmoji(string, index, inlineEmoji);
+
+ return url && isReplacable
? ``
@@ -89,14 +131,19 @@ export function performEmojiUnescape(string, opts) {
}
export function performEmojiEscape(string, opts) {
- return string.replace(unicodeRegexp, m => {
- if (!!translations[m]) {
- return opts.emojiShortcuts ? `:${translations[m]}:` : m;
- } else if (!!replacements[m]) {
- return `:${replacements[m]}:`;
- } else {
- return m;
+ const inlineEmoji = opts.inlineEmoji;
+ const regexp = unicodeRegexp(inlineEmoji);
+
+ return string.replace(regexp, (m, index) => {
+ if (isReplacableInlineEmoji(string, index, inlineEmoji)) {
+ if (!!translations[m]) {
+ return opts.emojiShortcuts ? `:${translations[m]}:` : m;
+ } else if (!!replacements[m]) {
+ return `:${replacements[m]}:`;
+ }
}
+
+ return m;
});
return string;
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
index e1c9a31bf08..6c2860577ae 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
@@ -120,7 +120,8 @@ const rule = {
title = performEmojiUnescape(topicInfo.title, {
getURL: options.getURL,
emojiSet: options.emojiSet,
- enableEmojiShortcuts: options.enableEmojiShortcuts
+ enableEmojiShortcuts: options.enableEmojiShortcuts,
+ inlineEmoji: options.inlineEmoji
});
}
@@ -156,6 +157,7 @@ export function setup(helper) {
opts.enableEmoji = siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set;
opts.enableEmojiShortcuts = siteSettings.enable_emoji_shortcuts;
+ opts.inlineEmoji = siteSettings.enable_inline_emoji_translation;
});
helper.registerPlugin(md => {
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 761432b26b3..b01117733a1 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -218,6 +218,7 @@ module PrettyText
set = SiteSetting.emoji_set.inspect
custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json
+
protect do
v8.eval(<<~JS)
__paths = #{paths_json};
@@ -225,7 +226,8 @@ module PrettyText
getURL: __getURL,
emojiSet: #{set},
customEmoji: #{custom},
- enableEmojiShortcuts: #{SiteSetting.enable_emoji_shortcuts}
+ enableEmojiShortcuts: #{SiteSetting.enable_emoji_shortcuts},
+ inlineEmoji: #{SiteSetting.enable_inline_emoji_translation}
});
JS
end
@@ -238,7 +240,10 @@ module PrettyText
protect do
v8.eval(<<~JS)
- __performEmojiEscape(#{title.inspect}, { emojiShortcuts: #{replace_emoji_shortcuts} });
+ __performEmojiEscape(#{title.inspect}, {
+ emojiShortcuts: #{replace_emoji_shortcuts},
+ inlineEmoji: #{SiteSetting.enable_inline_emoji_translation}
+ });
JS
end
end
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index e3932127954..26e2563cd88 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -308,6 +308,7 @@ describe Topic do
let(:topic_emoji) { build_topic_with_title("I 💖 candy alot") }
let(:topic_modifier_emoji) { build_topic_with_title("I 👨🌾 candy alot") }
let(:topic_shortcut_emoji) { build_topic_with_title("I love candy :)") }
+ let(:topic_inline_emoji) { build_topic_with_title("Hello😊World") }
it "escapes script contents" do
expect(topic_script.fancy_title).to eq("Topic with <script>alert(‘title’)</script> script in its title")
@@ -359,6 +360,16 @@ describe Topic do
expect(topic_shortcut_emoji.fancy_title).to eq("I love candy :)")
end
end
+
+ it "keeps inline emojis if inline emoji setting disabled" do
+ SiteSetting.enable_inline_emoji_translation = false
+ expect(topic_inline_emoji.fancy_title).to eq("Hello😊World")
+ end
+
+ it "expands inline emojis if inline emoji setting enabled" do
+ SiteSetting.enable_inline_emoji_translation = true
+ expect(topic_inline_emoji.fancy_title).to eq("Hello:blush:World")
+ end
end
context 'fancy title' do
diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6
index d856cc9eb1f..0e293e20f74 100644
--- a/test/javascripts/acceptance/topic-test.js.es6
+++ b/test/javascripts/acceptance/topic-test.js.es6
@@ -186,6 +186,27 @@ QUnit.test("Updating the topic title with unicode emojis", async assert => {
);
});
+QUnit.test(
+ "Updating the topic title with unicode emojis without whitespaces",
+ async assert => {
+ Discourse.SiteSettings.enable_inline_emoji_translation = true;
+ await visit("/t/internationalization-localization/280");
+ await click("#topic-title .d-icon-pencil-alt");
+
+ await fillIn("#edit-title", "Test🙂Title");
+
+ await click("#topic-title .submit-edit");
+
+ assert.equal(
+ find(".fancy-title")
+ .html()
+ .trim(),
+ `TestTitle`,
+ "it displays the new title with escaped unicode emojis"
+ );
+ }
+);
+
acceptance("Topic featured links", {
loggedIn: true,
settings: {
diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js
index 5ca4cbb867b..25568eda89d 100644
--- a/test/javascripts/helpers/site-settings.js
+++ b/test/javascripts/helpers/site-settings.js
@@ -93,6 +93,8 @@ Discourse.SiteSettingsOriginal = {
enable_emoji: true,
enable_emoji_shortcuts: true,
emoji_set: "emoji_one",
+ enable_emoji_shortcuts: true,
+ enable_inline_emoji_translation: false,
desktop_category_page_style: "categories_and_latest_topics",
enable_mentions: true,
enable_personal_messages: true,
diff --git a/test/javascripts/lib/emoji-test.js.es6 b/test/javascripts/lib/emoji-test.js.es6
index 69244d06c49..ca06ce07561 100644
--- a/test/javascripts/lib/emoji-test.js.es6
+++ b/test/javascripts/lib/emoji-test.js.es6
@@ -92,6 +92,41 @@ QUnit.test("emojiUnescape", assert => {
"no emoticons when emoji shortcuts are disabled",
{ enable_emoji_shortcuts: false }
);
+ testUnescape(
+ "Hello 😊 World",
+ `Hello World`,
+ "emoji from Unicode emoji"
+ );
+ testUnescape(
+ "Hello😊World",
+ "Hello😊World",
+ "keeps Unicode emoji when inline translation disabled",
+ {
+ enable_inline_emoji_translation: false
+ }
+ );
+ testUnescape(
+ "Hello😊World",
+ `HelloWorld`,
+ "emoji from Unicode emoji when inline translation enabled",
+ {
+ enable_inline_emoji_translation: true
+ }
+ );
+ testUnescape(
+ "hi:smile:",
+ "hi:smile:",
+ "no emojis when inline translation disabled",
+ {
+ enable_inline_emoji_translation: false
+ }
+ );
+ testUnescape(
+ "hi:smile:",
+ `hi`,
+ "emoji when inline translation enabled",
+ { enable_inline_emoji_translation: true }
+ );
});
QUnit.test("Emoji search", assert => {