TelegramPWA/src/util/emoji.ts
2023-04-25 17:27:10 +04:00

125 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { requestMutation } from '../lib/fasterdom/fasterdom';
import EMOJI_REGEX, { removeVS16s } from '../lib/twemojiRegex';
import withCache from './withCache';
// Due to the fact that emoji from Apple do not contain some characters, it is necessary to remove them from emoji-data
// https://github.com/iamcal/emoji-data/issues/136
const EXCLUDE_EMOJIS = ['female_sign', 'male_sign', 'medical_symbol'];
const ISO_FLAGS_OFFSET = 127397;
export type EmojiRawData = typeof import('emoji-data-ios/emoji-data.json');
export type EmojiModule = { default: EmojiRawData };
export type EmojiData = {
categories: Array<EmojiCategory>;
emojis: Record<string, Emoji>;
};
// Non-standard variations of emojis, used on some devices
const EMOJI_EXCEPTIONS: [string | RegExp, string][] = [
[/\u{1f3f3}\u200d\u{1f308}/gu, '\u{1f3f3}\ufe0f\u200d\u{1f308}'], // 🏳🌈
[/\u{1f3f3}\u200d\u26a7\ufe0f/gu, '\u{1f3f3}\ufe0f\u200d\u26a7\ufe0f'], // 🏳
[/\u{1f937}\u200d\u2642[^\ufe0f]/gu, '\u{1f937}\u200d\u2642\ufe0f'], // 🤷
];
function unifiedToNative(unified: string) {
const unicodes = unified.split('-');
const codePoints = unicodes.map((i) => parseInt(i, 16));
return String.fromCodePoint(...codePoints);
}
export const LOADED_EMOJIS = new Set<string>();
export function handleEmojiLoad(event: React.SyntheticEvent<HTMLImageElement>) {
const emoji = event.currentTarget;
LOADED_EMOJIS.add(event.currentTarget.dataset.path!);
requestMutation(() => {
emoji.classList.add('open');
});
}
export function fixNonStandardEmoji(text: string) {
// Non-standard sequences typically parsed as separate emojis, so no need to fix text without any
if (!text.match(EMOJI_REGEX)) return text;
// eslint-disable-next-line no-restricted-syntax
for (const [regex, replacement] of EMOJI_EXCEPTIONS) {
text = text.replace(regex, replacement);
}
return text;
}
export function nativeToUnified(emoji: string) {
let code;
if (emoji.length === 1) {
code = emoji.charCodeAt(0).toString(16).padStart(4, '0');
} else {
const pairs = [];
for (let i = 0; i < emoji.length; i++) {
if (emoji.charCodeAt(i) >= 0xd800 && emoji.charCodeAt(i) <= 0xdbff) {
if (emoji.charCodeAt(i + 1) >= 0xdc00 && emoji.charCodeAt(i + 1) <= 0xdfff) {
pairs.push(
(emoji.charCodeAt(i) - 0xd800) * 0x400
+ (emoji.charCodeAt(i + 1) - 0xdc00) + 0x10000,
);
}
} else if (emoji.charCodeAt(i) < 0xd800 || emoji.charCodeAt(i) > 0xdfff) {
pairs.push(emoji.charCodeAt(i));
}
}
code = pairs.map((x) => x.toString(16).padStart(4, '0')).join('-');
}
return code;
}
function nativeToUnifiedExtended(emoji: string) {
return nativeToUnified(removeVS16s(emoji));
}
export const nativeToUnifiedExtendedWithCache = withCache(nativeToUnifiedExtended);
export function uncompressEmoji(data: EmojiRawData): EmojiData {
const emojiData: EmojiData = { categories: [], emojis: {} };
for (let i = 0; i < data.length; i += 2) {
const category = {
id: data[i][0],
name: data[i][1],
emojis: [],
} as EmojiCategory;
for (let j = 0; j < data[i + 1].length; j++) {
const emojiRaw = data[i + 1][j];
if (!EXCLUDE_EMOJIS.includes(emojiRaw[1][0])) {
category.emojis.push(emojiRaw[1][0]);
emojiData.emojis[emojiRaw[1][0]] = {
id: emojiRaw[1][0],
names: emojiRaw[1] as string[],
native: unifiedToNative(emojiRaw[0] as string),
image: (emojiRaw[0] as string).toLowerCase(),
};
}
}
emojiData.categories.push(category);
}
return emojiData;
}
export function isoToEmoji(iso: string) {
const code = iso.toUpperCase();
if (!/^[A-Z]{2}$/.test(code)) return iso;
const codePoints = [...code].map((c) => c.codePointAt(0)! + ISO_FLAGS_OFFSET);
return String.fromCodePoint(...codePoints);
}