272 lines
6.3 KiB
JavaScript
272 lines
6.3 KiB
JavaScript
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 = {};
|
|
|
|
export function buildReplacementsList(emojiReplacements) {
|
|
return Object.keys(emojiReplacements)
|
|
.sort()
|
|
.reverse()
|
|
.map((emoji) => {
|
|
return emoji
|
|
.split("")
|
|
.map((chr) => {
|
|
return "\\u" + chr.charCodeAt(0).toString(16).padStart(4, "0");
|
|
})
|
|
.join("");
|
|
})
|
|
.join("|");
|
|
}
|
|
|
|
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));
|
|
|
|
// 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(`(?:${replacementList()})$`).test(beforeEmoji)
|
|
);
|
|
}
|
|
|
|
export function performEmojiUnescape(string, opts) {
|
|
if (!string) {
|
|
return;
|
|
}
|
|
|
|
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 && !!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, inlineEmoji);
|
|
|
|
return url && isReplacable
|
|
? `<img width="20" height="20" src='${url}' ${
|
|
opts.skipTitle ? "" : `title='${emojiVal}'`
|
|
} ${
|
|
opts.lazy ? "loading='lazy' " : ""
|
|
}alt='${emojiVal}' class='${classes}'>`
|
|
: m;
|
|
});
|
|
}
|
|
|
|
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 (!!allTranslations[m]) {
|
|
return opts.emojiShortcuts ? `:${allTranslations[m]}:` : m;
|
|
} else if (!!replacements[m]) {
|
|
return `:${replacements[m]}:`;
|
|
}
|
|
}
|
|
|
|
return m;
|
|
});
|
|
}
|
|
|
|
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(/([^:]+):?/);
|
|
if (
|
|
noToneMatch &&
|
|
!url &&
|
|
(emojiHash.hasOwnProperty(noToneMatch[1]) ||
|
|
aliasHash.hasOwnProperty(noToneMatch[1]))
|
|
) {
|
|
url = opts.getURL(
|
|
`/images/emoji/${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 ||
|
|
[
|
|
...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);
|
|
}
|
|
}
|
|
|
|
if (searchAliases[term]) {
|
|
results.push.apply(results, searchAliases[term]);
|
|
}
|
|
|
|
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) {
|
|
const match = term.split(":").filter(Boolean)[0];
|
|
if (match) {
|
|
return tonableEmojis.indexOf(match) !== -1;
|
|
}
|
|
return false;
|
|
}
|