UX: Better emoji escaping for topic title (#7176)

This commit is contained in:
Tim Lange 2019-03-19 09:33:10 +01:00 committed by Régis Hanol
parent dd142eec03
commit 0d9bc0aaa6
7 changed files with 77 additions and 25 deletions

View File

@ -3,7 +3,8 @@ import {
aliases, aliases,
searchAliases, searchAliases,
translations, translations,
tonableEmojis tonableEmojis,
replacements
} from "pretty-text/emoji/data"; } from "pretty-text/emoji/data";
import { IMAGE_VERSION } from "pretty-text/emoji/version"; import { IMAGE_VERSION } from "pretty-text/emoji/version";
@ -34,25 +35,48 @@ export function performEmojiUnescape(string, opts) {
return; return;
} }
// this can be further improved by supporting matches of emoticons that don't begin with a colon return string.replace(/[\u1000-\uFFFF]+|\B:[^\s:]+(?::t\d)?:?\B/g, m => {
if (string.indexOf(":") >= 0) { const isEmoticon = !!translations[m];
return string.replace(/\B:[^\s:]+(?::t\d)?:?\B/g, m => { const isUnicodeEmoticon = !!replacements[m];
const isEmoticon = !!translations[m]; let emojiVal;
const emojiVal = isEmoticon ? translations[m] : m.slice(1, m.length - 1); if (isEmoticon) {
const hasEndingColon = m.lastIndexOf(":") === m.length - 1; emojiVal = translations[m];
const url = buildEmojiUrl(emojiVal, opts); } else if (isUnicodeEmoticon) {
const classes = isCustomEmoji(emojiVal, opts) emojiVal = replacements[m];
? "emoji emoji-custom" } else {
: "emoji"; emojiVal = m.slice(1, m.length - 1);
}
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
const url = buildEmojiUrl(emojiVal, opts);
const classes = isCustomEmoji(emojiVal, opts)
? "emoji emoji-custom"
: "emoji";
return url && (isEmoticon || hasEndingColon) return url && (isEmoticon || hasEndingColon || isUnicodeEmoticon)
? `<img src='${url}' ${ ? `<img src='${url}' ${
opts.skipTitle ? "" : `title='${emojiVal}'` opts.skipTitle ? "" : `title='${emojiVal}'`
} alt='${emojiVal}' class='${classes}'>` } alt='${emojiVal}' class='${classes}'>`
: m; : m;
}); });
return string;
}
export function performEmojiEscape(string) {
if (!string) {
return;
} }
return string.replace(/[\u1000-\uFFFF]+|\B:[^\s:]+(?::t\d)?:?\B/g, m => {
if (!!translations[m]) {
return ":" + translations[m] + ":";
} else if (!!replacements[m]) {
return ":" + replacements[m] + ":";
} else {
return m;
}
});
return string; return string;
} }

View File

@ -3,3 +3,4 @@ export const tonableEmojis = <%= Emoji.tonable_emojis.flatten.inspect %>;
export const aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>; export const aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
export const searchAliases = <%= Emoji.search_aliases.inspect.gsub("=>", ":") %>; export const searchAliases = <%= Emoji.search_aliases.inspect.gsub("=>", ":") %>;
export const translations = <%= Emoji.translations.inspect.gsub("=>", ":") %>; export const translations = <%= Emoji.translations.inspect.gsub("=>", ":") %>;
export const replacements = <%= Emoji.unicode_replacements_json %>;

View File

@ -157,13 +157,7 @@ class Emoji
end end
def self.unicode_unescape(string) def self.unicode_unescape(string)
string.each_char.map do |c| PrettyText.escape_emoji(string)
if str = unicode_replacements[c]
":#{str}:"
else
c
end
end.join
end end
def self.gsub_emoji_to_unicode(str) def self.gsub_emoji_to_unicode(str)

View File

@ -227,7 +227,7 @@ module PrettyText
end end
def self.unescape_emoji(title) def self.unescape_emoji(title)
return title unless SiteSetting.enable_emoji? return title unless SiteSetting.enable_emoji? && title
set = SiteSetting.emoji_set.inspect set = SiteSetting.emoji_set.inspect
custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json
@ -239,6 +239,16 @@ module PrettyText
end end
end end
def self.escape_emoji(title)
return unless title
protect do
v8.eval(<<~JS)
__performEmojiEscape(#{title.inspect});
JS
end
end
def self.cook(text, opts = {}) def self.cook(text, opts = {})
options = opts.dup options = opts.dup

View File

@ -1,6 +1,7 @@
__PrettyText = require("pretty-text/pretty-text").default; __PrettyText = require("pretty-text/pretty-text").default;
__buildOptions = require("pretty-text/pretty-text").buildOptions; __buildOptions = require("pretty-text/pretty-text").buildOptions;
__performEmojiUnescape = require("pretty-text/emoji").performEmojiUnescape; __performEmojiUnescape = require("pretty-text/emoji").performEmojiUnescape;
__performEmojiEscape = require("pretty-text/emoji").performEmojiEscape;
__utils = require("discourse/lib/utilities"); __utils = require("discourse/lib/utilities");

View File

@ -279,6 +279,7 @@ describe Topic do
let(:topic_image) { build_topic_with_title("Topic with <img src='something'> image in its title") } let(:topic_image) { build_topic_with_title("Topic with <img src='something'> image in its title") }
let(:topic_script) { build_topic_with_title("Topic with <script>alert('title')</script> script in its title") } let(:topic_script) { build_topic_with_title("Topic with <script>alert('title')</script> script in its title") }
let(:topic_emoji) { build_topic_with_title("I 💖 candy alot") } let(:topic_emoji) { build_topic_with_title("I 💖 candy alot") }
let(:topic_modifier_emoji) { build_topic_with_title("I 👨‍🌾 candy alot") }
it "escapes script contents" do it "escapes script contents" do
expect(topic_script.fancy_title).to eq("Topic with &lt;script&gt;alert(&lsquo;title&rsquo;)&lt;/script&gt; script in its title") expect(topic_script.fancy_title).to eq("Topic with &lt;script&gt;alert(&lsquo;title&rsquo;)&lt;/script&gt; script in its title")
@ -288,6 +289,10 @@ describe Topic do
expect(topic_emoji.fancy_title).to eq("I :sparkling_heart: candy alot") expect(topic_emoji.fancy_title).to eq("I :sparkling_heart: candy alot")
end end
it "keeps combined emojis" do
expect(topic_modifier_emoji.fancy_title).to eq("I :man_farmer: candy alot")
end
it "escapes bold contents" do it "escapes bold contents" do
expect(topic_bold.fancy_title).to eq("Topic with &lt;b&gt;bold&lt;/b&gt; text in its title") expect(topic_bold.fancy_title).to eq("Topic with &lt;b&gt;bold&lt;/b&gt; text in its title")
end end

View File

@ -184,6 +184,23 @@ QUnit.test("Updating the topic title with emojis", async assert => {
); );
}); });
QUnit.test("Updating the topic title with unicode emojis", async assert => {
await visit("/t/internationalization-localization/280");
await click("#topic-title .d-icon-pencil-alt");
await fillIn("#edit-title", "emojis title 👨‍🌾");
await click("#topic-title .submit-edit");
assert.equal(
find(".fancy-title")
.html()
.trim(),
`emojis title <img src="/images/emoji/emoji_one/man_farmer.png?v=${v}" title="man_farmer" alt="man_farmer" class="emoji">`,
"it displays the new title with escaped unicode emojis"
);
});
acceptance("Topic featured links", { acceptance("Topic featured links", {
loggedIn: true, loggedIn: true,
settings: { settings: {