mirror of
https://github.com/mastodon/mastodon.git
synced 2024-08-20 21:08:15 -07:00
Merge 8585e26aae
into a50c8e951f
This commit is contained in:
commit
f5c9f12295
3 changed files with 222 additions and 259 deletions
|
@ -8,7 +8,7 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||
|
||||
type Emojis = {
|
||||
export type Emojis = {
|
||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
||||
native: BaseEmoji['native'];
|
||||
search: Search;
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
|
||||
|
||||
import * as data from './emoji_mart_data_light';
|
||||
|
||||
const buildSearch = (data) => {
|
||||
const search = [];
|
||||
|
||||
let addToSearch = (strings, split) => {
|
||||
if (!strings) {
|
||||
return;
|
||||
}
|
||||
|
||||
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
|
||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
||||
s = s.toLowerCase();
|
||||
|
||||
if (search.indexOf(s) === -1) {
|
||||
search.push(s);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addToSearch(data.short_names, true);
|
||||
addToSearch(data.name, true);
|
||||
addToSearch(data.keywords, false);
|
||||
addToSearch(data.emoticons, false);
|
||||
|
||||
return search.join(',');
|
||||
};
|
||||
|
||||
const _String = String;
|
||||
|
||||
const stringFromCodePoint = _String.fromCodePoint || function () {
|
||||
let MAX_SIZE = 0x4000;
|
||||
let codeUnits = [];
|
||||
let highSurrogate;
|
||||
let lowSurrogate;
|
||||
let index = -1;
|
||||
let length = arguments.length;
|
||||
if (!length) {
|
||||
return '';
|
||||
}
|
||||
let result = '';
|
||||
while (++index < length) {
|
||||
let codePoint = Number(arguments[index]);
|
||||
if (
|
||||
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
|
||||
codePoint < 0 || // not a valid Unicode code point
|
||||
codePoint > 0x10FFFF || // not a valid Unicode code point
|
||||
Math.floor(codePoint) !== codePoint // not an integer
|
||||
) {
|
||||
throw RangeError('Invalid code point: ' + codePoint);
|
||||
}
|
||||
if (codePoint <= 0xFFFF) { // BMP code point
|
||||
codeUnits.push(codePoint);
|
||||
} else { // Astral code point; split in surrogate halves
|
||||
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
codePoint -= 0x10000;
|
||||
highSurrogate = (codePoint >> 10) + 0xD800;
|
||||
lowSurrogate = (codePoint % 0x400) + 0xDC00;
|
||||
codeUnits.push(highSurrogate, lowSurrogate);
|
||||
}
|
||||
if (index + 1 === length || codeUnits.length > MAX_SIZE) {
|
||||
result += String.fromCharCode.apply(null, codeUnits);
|
||||
codeUnits.length = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
const _JSON = JSON;
|
||||
|
||||
const COLONS_REGEX = /^(?::([^:]+):)(?::skin-tone-(\d):)?$/;
|
||||
const SKINS = [
|
||||
'1F3FA', '1F3FB', '1F3FC',
|
||||
'1F3FD', '1F3FE', '1F3FF',
|
||||
];
|
||||
|
||||
function unifiedToNative(unified) {
|
||||
let unicodes = unified.split('-'),
|
||||
codePoints = unicodes.map((u) => `0x${u}`);
|
||||
|
||||
return stringFromCodePoint.apply(null, codePoints);
|
||||
}
|
||||
|
||||
function sanitize(emoji) {
|
||||
let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji,
|
||||
id = emoji.id || short_names[0],
|
||||
colons = `:${id}:`;
|
||||
|
||||
if (custom) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
custom,
|
||||
imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (skin_tone) {
|
||||
colons += `:skin-tone-${skin_tone}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
unified: unified.toLowerCase(),
|
||||
skin: skin_tone || (skin_variations ? 1 : null),
|
||||
native: unifiedToNative(unified),
|
||||
};
|
||||
}
|
||||
|
||||
function getSanitizedData() {
|
||||
return sanitize(getData(...arguments));
|
||||
}
|
||||
|
||||
function getData(emoji, skin, set) {
|
||||
let emojiData = {};
|
||||
|
||||
if (typeof emoji === 'string') {
|
||||
let matches = emoji.match(COLONS_REGEX);
|
||||
|
||||
if (matches) {
|
||||
emoji = matches[1];
|
||||
|
||||
if (matches[2]) {
|
||||
skin = parseInt(matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.short_names, emoji)) {
|
||||
emoji = data.short_names[emoji];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, emoji)) {
|
||||
emojiData = data.emojis[emoji];
|
||||
}
|
||||
} else if (emoji.id) {
|
||||
if (Object.hasOwn(data.short_names, emoji.id)) {
|
||||
emoji.id = data.short_names[emoji.id];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, emoji.id)) {
|
||||
emojiData = data.emojis[emoji.id];
|
||||
skin = skin || emoji.skin;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(emojiData).length) {
|
||||
emojiData = emoji;
|
||||
emojiData.custom = true;
|
||||
|
||||
if (!emojiData.search) {
|
||||
emojiData.search = buildSearch(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
emojiData.emoticons = emojiData.emoticons || [];
|
||||
emojiData.variations = emojiData.variations || [];
|
||||
|
||||
if (emojiData.skin_variations && skin > 1 && set) {
|
||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
||||
|
||||
let skinKey = SKINS[skin - 1],
|
||||
variationData = emojiData.skin_variations[skinKey];
|
||||
|
||||
if (!variationData.variations && emojiData.variations) {
|
||||
delete emojiData.variations;
|
||||
}
|
||||
|
||||
if (variationData[`has_img_${set}`]) {
|
||||
emojiData.skin_tone = skin;
|
||||
|
||||
for (let k in variationData) {
|
||||
let v = variationData[k];
|
||||
emojiData[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (emojiData.variations && emojiData.variations.length) {
|
||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
||||
emojiData.unified = emojiData.variations.shift();
|
||||
}
|
||||
|
||||
return emojiData;
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return arr.reduce((acc, item) => {
|
||||
if (acc.indexOf(item) === -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function intersect(a, b) {
|
||||
const uniqA = uniq(a);
|
||||
const uniqB = uniq(b);
|
||||
|
||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||
}
|
||||
|
||||
function deepMerge(a, b) {
|
||||
let o = {};
|
||||
|
||||
for (let key in a) {
|
||||
let originalValue = a[key],
|
||||
value = originalValue;
|
||||
|
||||
if (Object.hasOwn(b, key)) {
|
||||
value = b[key];
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
value = deepMerge(originalValue, value);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
// https://github.com/sonicdoe/measure-scrollbar
|
||||
function measureScrollbar() {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.style.width = '100px';
|
||||
div.style.height = '100px';
|
||||
div.style.overflow = 'scroll';
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = '-9999px';
|
||||
|
||||
document.body.appendChild(div);
|
||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
||||
document.body.removeChild(div);
|
||||
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
export {
|
||||
getData,
|
||||
getSanitizedData,
|
||||
uniq,
|
||||
intersect,
|
||||
deepMerge,
|
||||
unifiedToNative,
|
||||
measureScrollbar,
|
||||
};
|
221
app/javascript/mastodon/features/emoji/emoji_utils.ts
Normal file
221
app/javascript/mastodon/features/emoji/emoji_utils.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
|
||||
|
||||
import type {
|
||||
BaseEmoji,
|
||||
CustomEmoji,
|
||||
EmojiSkin,
|
||||
PickerProps,
|
||||
} from 'emoji-mart';
|
||||
import type { Emoji, SkinVariation } from 'emoji-mart/dist-es/utils/data';
|
||||
|
||||
import * as data from './emoji_mart_data_light';
|
||||
|
||||
type Data = Pick<Emoji, 'short_names' | 'name' | 'keywords' | 'emoticons'>;
|
||||
|
||||
const buildSearch = (data: Data) => {
|
||||
const search: string[] = [];
|
||||
|
||||
const addToSearch = (strings: Data[keyof Data], split: boolean) => {
|
||||
if (!strings) {
|
||||
return;
|
||||
}
|
||||
|
||||
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
|
||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
||||
s = s.toLowerCase();
|
||||
|
||||
if (!search.includes(s)) {
|
||||
search.push(s);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addToSearch(data.short_names, true);
|
||||
addToSearch(data.name, true);
|
||||
addToSearch(data.keywords, false);
|
||||
addToSearch(data.emoticons, false);
|
||||
|
||||
return search.join(',');
|
||||
};
|
||||
|
||||
const COLONS_REGEX = /^(?::([^:]+):)(?::skin-tone-(\d):)?$/;
|
||||
const SKINS = ['1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
|
||||
|
||||
function unifiedToNative(unified: Emoji['unified']) {
|
||||
const unicodes = unified?.split('-') ?? [];
|
||||
const codePoints = unicodes.map((u) => +`0x${u}`);
|
||||
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
/*
|
||||
* `skin_tone` is used [here]{@link node_modules/emoji-mart/dist-es/utils/index.js#19}, but is not found in the [type definition]{@link node_modules/@types/emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index.d.ts}.
|
||||
* `emoji-mart` does not come with a built-in type, so you need to add a separate type with DefinitelyTyped.
|
||||
* The type and implementation have different maintainers and packages, so the installed versions of `@types/emoji-mart` and `emoji-mart` may not match.
|
||||
*/
|
||||
|
||||
interface SkinTone {
|
||||
skin_tone?: EmojiSkin;
|
||||
}
|
||||
|
||||
type RawEmoji = BaseEmoji &
|
||||
CustomEmoji &
|
||||
Pick<Emoji, 'skin_variations'> &
|
||||
Pick<PickerProps, 'custom'> &
|
||||
SkinTone;
|
||||
|
||||
function sanitize(
|
||||
emoji: RawEmoji,
|
||||
):
|
||||
| BaseEmoji
|
||||
| (Omit<CustomEmoji, 'short_names'> & Pick<PickerProps, 'custom'>) {
|
||||
const {
|
||||
name = '',
|
||||
short_names = [],
|
||||
skin_tone,
|
||||
skin_variations,
|
||||
emoticons = [],
|
||||
unified = '',
|
||||
custom,
|
||||
imageUrl,
|
||||
} = emoji;
|
||||
const id = emoji.id || short_names[0];
|
||||
|
||||
let colons = `:${id}:`;
|
||||
|
||||
if (custom) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
custom,
|
||||
imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (skin_tone) {
|
||||
colons += `:skin-tone-${skin_tone}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
unified: unified.toLowerCase(),
|
||||
skin: skin_tone ?? (skin_variations ? 1 : null),
|
||||
native: unifiedToNative(unified),
|
||||
};
|
||||
}
|
||||
|
||||
type GetDataArgs = [
|
||||
emoji: BaseEmoji | string,
|
||||
skin: EmojiSkin | null,
|
||||
set?: 'apple' | 'google' | 'twitter' | 'facebook' | 'emojione' | 'messenger',
|
||||
];
|
||||
|
||||
function getSanitizedData(...args: GetDataArgs) {
|
||||
return sanitize(getData(...args));
|
||||
}
|
||||
|
||||
function getData(...[emoji, skin, set]: GetDataArgs) {
|
||||
/*
|
||||
The version of [the referenced source code]{@link https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js} does not match that of DefinitelyTyped.
|
||||
It is also old, and non-existent properties have been added or removed, making it difficult to achieve type consistency.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
let emojiData: any = {};
|
||||
|
||||
if (typeof emoji === 'string') {
|
||||
const matches = emoji.match(COLONS_REGEX);
|
||||
|
||||
if (matches) {
|
||||
emoji = matches[1];
|
||||
|
||||
const int = parseInt(matches[2]);
|
||||
const isValid = (value: number): value is EmojiSkin =>
|
||||
([1, 2, 3, 4, 5, 6] satisfies EmojiSkin[]).some(
|
||||
(skin) => skin === value,
|
||||
);
|
||||
|
||||
if (isValid(int)) {
|
||||
skin = int;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.short_names, emoji)) {
|
||||
emoji = data.short_names[emoji];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, emoji)) {
|
||||
emojiData = data.emojis[emoji];
|
||||
}
|
||||
} else if (emoji.id) {
|
||||
if (Object.hasOwn(data.short_names, emoji.id)) {
|
||||
emoji.id = data.short_names[emoji.id];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, emoji.id)) {
|
||||
emojiData = data.emojis[emoji.id];
|
||||
skin = skin ?? emoji.skin;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(emojiData).length && typeof emoji === 'object') {
|
||||
emojiData = emoji;
|
||||
emojiData.custom = true;
|
||||
|
||||
if (!emojiData.search) {
|
||||
emojiData.search = buildSearch(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
emojiData.emoticons = emojiData.emoticons || [];
|
||||
emojiData.variations = emojiData.variations || [];
|
||||
|
||||
if (emojiData.skin_variations && skin && skin > 1 && set) {
|
||||
const skinKey = SKINS[skin - 1];
|
||||
const variationData = emojiData.skin_variations[skinKey];
|
||||
|
||||
if (variationData[`has_img_${set}`]) {
|
||||
emojiData.skin_tone = skin;
|
||||
|
||||
for (const k in variationData) {
|
||||
type K = keyof typeof emojiData;
|
||||
emojiData[k as K] = variationData[k as keyof SkinVariation];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (emojiData.variations.length) {
|
||||
emojiData.unified = emojiData.variations.shift();
|
||||
}
|
||||
|
||||
return emojiData as RawEmoji;
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||
}
|
||||
|
||||
// TODO: General array operations not related to emojis. Consider separating them into separate files.
|
||||
function uniq(arr: []) {
|
||||
return arr.reduce((acc, item) => {
|
||||
if (!acc.includes(item)) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// TODO: General array operations not related to emojis. Consider separating them into separate files.
|
||||
function intersect(a: [], b: []) {
|
||||
const uniqA = uniq(a);
|
||||
const uniqB = uniq(b);
|
||||
|
||||
return uniqA.filter((item) => uniqB.includes(item));
|
||||
}
|
||||
|
||||
export { getData, getSanitizedData, uniq, intersect, unifiedToNative };
|
Loading…
Reference in a new issue