discourse/app/assets/javascripts/pretty-text/addon/emoji.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

254 lines
20 KiB
JavaScript
Raw Normal View History

import {
aliases,
emojis,
replacements,
searchAliases,
tonableEmojis,
translations,
} from "pretty-text/emoji/data";
import { IMAGE_VERSION } from "pretty-text/emoji/version";
const extendedEmoji = {};
export function registerEmoji(code, url, group) {
code = code.toLowerCase();
extendedEmoji[code] = { url, group };
}
export function extendedEmojiList() {
return extendedEmoji;
}
const emojiHash = {};
// https://github.com/mathiasbynens/emoji-regex/blob/main/text.js
export const emojiReplacementRegex =
"\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])
function textEmojiRegex(inlineEmoji) {
return inlineEmoji ? /:[^\s:]+(?::t\d)?:?/g : /\B:[^\s:]+(?::t\d)?:?\B/g;
}
2019-03-28 17:31:27 -04:00
// add all default emojis
emojis.forEach((code) => (emojiHash[code] = true));
// and their aliases
const aliasHash = {};
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);
return (
beforeEmoji.length === 0 ||
/(?:\s|[>.,\/#!$%^&*;:{}=\-_`~()])$/.test(beforeEmoji) ||
new RegExp(`(?:${emojiReplacementRegex})$`, "g").test(beforeEmoji)
);
}
export function performEmojiUnescape(string, opts) {
if (!string) {
return;
}
const allTranslations = Object.assign(
{},
translations,
opts.customEmojiTranslation || {}
);
const replacementFunction = (m, index) => {
const isEmoticon = opts.enableEmojiShortcuts && !!allTranslations[m];
const isUnicodeEmoticon = !!replacements[m];
let emojiVal;
if (isEmoticon) {
emojiVal = allTranslations[m];
} else if (isUnicodeEmoticon) {
emojiVal = replacements[m];
} else {
emojiVal = m.slice(1, m.length - 1);
}
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
const url = buildEmojiUrl(emojiVal, opts);
let classes = isCustomEmoji(emojiVal, opts)
? "emoji emoji-custom"
: "emoji";
if (opts.class) {
classes = `${classes} ${opts.class}`;
}
const isReplacable =
(isEmoticon || hasEndingColon || isUnicodeEmoticon) &&
isReplacableInlineEmoji(string, index, opts.inlineEmoji);
return url && isReplacable
? `<img width="20" height="20" src='${url}' ${
opts.skipTitle ? "" : `title='${emojiVal}'`
} ${
opts.lazy ? "loading='lazy' " : ""
}alt='${emojiVal}' class='${classes}'>`
: m;
};
return string
.replace(new RegExp(emojiReplacementRegex, "g"), replacementFunction)
.replace(textEmojiRegex(opts.inlineEmoji), replacementFunction);
}
export function performEmojiEscape(string, opts) {
const allTranslations = Object.assign(
{},
translations,
opts.customEmojiTranslation || {}
);
const replacementFunction = (m, index) => {
if (isReplacableInlineEmoji(string, index, opts.inlineEmoji)) {
if (!!allTranslations[m]) {
return opts.emojiShortcuts ? `:${allTranslations[m]}:` : m;
} else if (!!replacements[m]) {
return `:${replacements[m]}:`;
}
}
return m;
};
return string
.replace(new RegExp(emojiReplacementRegex, "g"), replacementFunction)
.replace(textEmojiRegex(opts.inlineEmoji), replacementFunction);
}
export function isCustomEmoji(code, opts) {
code = code.toLowerCase();
if (extendedEmoji.hasOwnProperty(code)) {
return true;
}
if (opts && opts.customEmoji && opts.customEmoji.hasOwnProperty(code)) {
return true;
}
return false;
}
export function buildEmojiUrl(code, opts) {
let url;
code = String(code).toLowerCase();
if (extendedEmoji.hasOwnProperty(code)) {
url = extendedEmoji[code].url;
}
if (opts && opts.customEmoji && opts.customEmoji[code]) {
url = opts.customEmoji[code].url || opts.customEmoji[code];
}
const noToneMatch = code.match(/([^:]+):?/);
let emojiBasePath = "/images/emoji";
if (opts.emojiCDNUrl) {
emojiBasePath = opts.emojiCDNUrl;
}
if (
noToneMatch &&
!url &&
(emojiHash.hasOwnProperty(noToneMatch[1]) ||
aliasHash.hasOwnProperty(noToneMatch[1]))
) {
url = opts.getURL(
`${emojiBasePath}/${opts.emojiSet}/${code.replace(/:t/, "/")}.png`
);
}
if (url) {
url = url + "?v=" + IMAGE_VERSION;
}
return url;
}
export function emojiExists(code) {
code = code.toLowerCase();
return !!(
extendedEmoji.hasOwnProperty(code) ||
emojiHash.hasOwnProperty(code) ||
aliasHash.hasOwnProperty(code)
);
}
let toSearch;
export function emojiSearch(term, options) {
const maxResults = (options && options["maxResults"]) || -1;
const diversity = options && options.diversity;
if (maxResults === 0) {
return [];
}
toSearch =
toSearch ||
2020-09-01 14:45:44 -04:00
[
...Object.keys(emojiHash),
...Object.keys(extendedEmoji),
...Object.keys(aliasHash),
].sort();
const results = [];
function addResult(t) {
const val = aliasHash[t] || t;
if (results.indexOf(val) === -1) {
if (diversity && diversity > 1 && isSkinTonableEmoji(val)) {
results.push(`${val}:t${diversity}`);
} else {
results.push(val);
}
}
}
// if term matches from beginning
for (let i = 0; i < toSearch.length; i++) {
const item = toSearch[i];
if (item.indexOf(term) === 0) {
addResult(item);
}
}
for (const [key, value] of Object.entries(searchAliases)) {
if (key.startsWith(term)) {
for (const emoji of value) {
addResult(emoji);
}
}
}
for (let i = 0; i < toSearch.length; i++) {
const item = toSearch[i];
if (item.indexOf(term) > 0) {
addResult(item);
}
}
if (maxResults === -1) {
return results;
} else {
return results.slice(0, maxResults);
}
}
export function isSkinTonableEmoji(term) {
2020-09-01 16:47:22 -04:00
const match = term.split(":").filter(Boolean)[0];
if (match) {
return tonableEmojis.indexOf(match) !== -1;
}
return false;
}