summaryrefslogtreecommitdiff
path: root/packages/frontend-shared/js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend-shared/js')
-rw-r--r--packages/frontend-shared/js/const.ts137
-rw-r--r--packages/frontend-shared/js/embed-page.ts97
-rw-r--r--packages/frontend-shared/js/emoji-base.ts25
-rw-r--r--packages/frontend-shared/js/emojilist.json1805
-rw-r--r--packages/frontend-shared/js/emojilist.ts73
-rw-r--r--packages/frontend-shared/js/extract-avg-color-from-blurhash.ts14
-rw-r--r--packages/frontend-shared/js/i18n.ts251
-rw-r--r--packages/frontend-shared/js/media-proxy.ts63
-rw-r--r--packages/frontend-shared/js/scroll.ts144
-rw-r--r--packages/frontend-shared/js/url.ts28
-rw-r--r--packages/frontend-shared/js/use-document-visibility.ts25
-rw-r--r--packages/frontend-shared/js/use-interval.ts46
12 files changed, 2708 insertions, 0 deletions
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
new file mode 100644
index 0000000000..8391fb638c
--- /dev/null
+++ b/packages/frontend-shared/js/const.ts
@@ -0,0 +1,137 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// ブラウザで直接表示することを許可するファイルの種類のリスト
+// ここに含まれないものは application/octet-stream としてレスポンスされる
+// SVGはXSSを生むので許可しない
+export const FILE_TYPE_BROWSERSAFE = [
+ // Images
+ 'image/png',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/webp',
+ 'image/avif',
+ 'image/apng',
+ 'image/bmp',
+ 'image/tiff',
+ 'image/x-icon',
+
+ // OggS
+ 'audio/opus',
+ 'video/ogg',
+ 'audio/ogg',
+ 'application/ogg',
+
+ // ISO/IEC base media file format
+ 'video/quicktime',
+ 'video/mp4',
+ 'audio/mp4',
+ 'video/x-m4v',
+ 'audio/x-m4a',
+ 'video/3gpp',
+ 'video/3gpp2',
+
+ 'video/mpeg',
+ 'audio/mpeg',
+
+ 'video/webm',
+ 'audio/webm',
+
+ 'audio/aac',
+
+ // see https://github.com/misskey-dev/misskey/pull/10686
+ 'audio/flac',
+ 'audio/wav',
+ // backward compatibility
+ 'audio/x-flac',
+ 'audio/vnd.wave',
+];
+/*
+https://github.com/sindresorhus/file-type/blob/main/supported.js
+https://github.com/sindresorhus/file-type/blob/main/core.js
+https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
+*/
+
+export const notificationTypes = [
+ 'note',
+ 'follow',
+ 'mention',
+ 'reply',
+ 'renote',
+ 'quote',
+ 'reaction',
+ 'pollEnded',
+ 'receiveFollowRequest',
+ 'followRequestAccepted',
+ 'roleAssigned',
+ 'achievementEarned',
+ 'app',
+] as const;
+export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
+
+export const ROLE_POLICIES = [
+ 'gtlAvailable',
+ 'ltlAvailable',
+ 'canPublicNote',
+ 'mentionLimit',
+ 'canInvite',
+ 'inviteLimit',
+ 'inviteLimitCycle',
+ 'inviteExpirationTime',
+ 'canManageCustomEmojis',
+ 'canManageAvatarDecorations',
+ 'canSearchNotes',
+ 'canUseTranslator',
+ 'canHideAds',
+ 'driveCapacityMb',
+ 'alwaysMarkNsfw',
+ 'canUpdateBioMedia',
+ 'pinLimit',
+ 'antennaLimit',
+ 'wordMuteLimit',
+ 'webhookLimit',
+ 'clipLimit',
+ 'noteEachClipsLimit',
+ 'userListLimit',
+ 'userEachUserListsLimit',
+ 'rateLimitFactor',
+ 'avatarDecorationLimit',
+] as const;
+
+// なんか動かない
+//export const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
+//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM');
+export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
+export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
+
+export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
+export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
+export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
+
+export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
+export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
+ tada: ['speed=', 'delay='],
+ jelly: ['speed=', 'delay='],
+ twitch: ['speed=', 'delay='],
+ shake: ['speed=', 'delay='],
+ spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'],
+ jump: ['speed=', 'delay='],
+ bounce: ['speed=', 'delay='],
+ flip: ['h', 'v'],
+ x2: [],
+ x3: [],
+ x4: [],
+ scale: ['x=', 'y='],
+ position: ['x=', 'y='],
+ fg: ['color='],
+ bg: ['color='],
+ border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
+ font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
+ blur: [],
+ rainbow: ['speed=', 'delay='],
+ rotate: ['deg='],
+ ruby: [],
+ unixtime: [],
+};
diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts
new file mode 100644
index 0000000000..d5555a98c3
--- /dev/null
+++ b/packages/frontend-shared/js/embed-page.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+//#region Embed関連の定義
+
+/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */
+const embeddableEntities = [
+ 'notes',
+ 'user-timeline',
+ 'clips',
+ 'tags',
+] as const;
+
+/** 埋め込みの対象となるエンティティ */
+export type EmbeddableEntity = typeof embeddableEntities[number];
+
+/** 内部でスクロールがあるページ */
+export const embedRouteWithScrollbar: EmbeddableEntity[] = [
+ 'clips',
+ 'tags',
+ 'user-timeline',
+];
+
+/** 埋め込みコードのパラメータ */
+export type EmbedParams = {
+ maxHeight?: number;
+ colorMode?: 'light' | 'dark';
+ rounded?: boolean;
+ border?: boolean;
+ autoload?: boolean;
+ header?: boolean;
+};
+
+/** 正規化されたパラメータ */
+export type ParsedEmbedParams = Required<Omit<EmbedParams, 'maxHeight' | 'colorMode'>> & Pick<EmbedParams, 'maxHeight' | 'colorMode'>;
+
+/** パラメータのデフォルトの値 */
+export const defaultEmbedParams = {
+ maxHeight: undefined,
+ colorMode: undefined,
+ rounded: true,
+ border: true,
+ autoload: false,
+ header: true,
+} as const satisfies EmbedParams;
+
+//#endregion
+
+/**
+ * パラメータを正規化する(埋め込みページ初期化用)
+ * @param searchParams URLSearchParamsもしくはクエリ文字列
+ * @returns 正規化されたパラメータ
+ */
+export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams {
+ let _searchParams: URLSearchParams;
+ if (typeof searchParams === 'string') {
+ _searchParams = new URLSearchParams(searchParams);
+ } else if (searchParams instanceof URLSearchParams) {
+ _searchParams = searchParams;
+ } else {
+ throw new Error('searchParams must be URLSearchParams or string');
+ }
+
+ function convertBoolean(value: string | null): boolean | undefined {
+ if (value === 'true') {
+ return true;
+ } else if (value === 'false') {
+ return false;
+ }
+ return undefined;
+ }
+
+ function convertNumber(value: string | null): number | undefined {
+ if (value != null && !isNaN(Number(value))) {
+ return Number(value);
+ }
+ return undefined;
+ }
+
+ function convertColorMode(value: string | null): 'light' | 'dark' | undefined {
+ if (value != null && ['light', 'dark'].includes(value)) {
+ return value as 'light' | 'dark';
+ }
+ return undefined;
+ }
+
+ return {
+ maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight,
+ colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode,
+ rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded,
+ border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border,
+ autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload,
+ header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header,
+ };
+}
diff --git a/packages/frontend-shared/js/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts
new file mode 100644
index 0000000000..a01540a3e4
--- /dev/null
+++ b/packages/frontend-shared/js/emoji-base.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const twemojiSvgBase = '/twemoji';
+const fluentEmojiPngBase = '/fluent-emoji';
+
+export function char2twemojiFilePath(char: string): string {
+ let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
+ if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
+ codes = codes.filter(x => x && x.length);
+ const fileName = codes.join('-');
+ return `${twemojiSvgBase}/${fileName}.svg`;
+}
+
+export function char2fluentEmojiFilePath(char: string): string {
+ let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
+ // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
+ if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
+ if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
+ codes = codes.filter(x => x && x.length);
+ const fileName = codes.map(x => x!.padStart(4, '0')).join('-');
+ return `${fluentEmojiPngBase}/${fileName}.png`;
+}
diff --git a/packages/frontend-shared/js/emojilist.json b/packages/frontend-shared/js/emojilist.json
new file mode 100644
index 0000000000..75d5c34d71
--- /dev/null
+++ b/packages/frontend-shared/js/emojilist.json
@@ -0,0 +1,1805 @@
+[
+ ["😀", "grinning", 0],
+ ["😬", "grimacing", 0],
+ ["😁", "grin", 0],
+ ["😂", "joy", 0],
+ ["🤣", "rofl", 0],
+ ["🥳", "partying", 0],
+ ["😃", "smiley", 0],
+ ["😄", "smile", 0],
+ ["😅", "sweat_smile", 0],
+ ["🥲", "smiling_face_with_tear", 0],
+ ["😆", "laughing", 0],
+ ["😇", "innocent", 0],
+ ["😉", "wink", 0],
+ ["😊", "blush", 0],
+ ["🙂", "slightly_smiling_face", 0],
+ ["🙃", "upside_down_face", 0],
+ ["☺️", "relaxed", 0],
+ ["😋", "yum", 0],
+ ["😌", "relieved", 0],
+ ["😍", "heart_eyes", 0],
+ ["🥰", "smiling_face_with_three_hearts", 0],
+ ["😘", "kissing_heart", 0],
+ ["😗", "kissing", 0],
+ ["😙", "kissing_smiling_eyes", 0],
+ ["😚", "kissing_closed_eyes", 0],
+ ["😜", "stuck_out_tongue_winking_eye", 0],
+ ["🤪", "zany", 0],
+ ["🤨", "raised_eyebrow", 0],
+ ["🧐", "monocle", 0],
+ ["😝", "stuck_out_tongue_closed_eyes", 0],
+ ["😛", "stuck_out_tongue", 0],
+ ["🤑", "money_mouth_face", 0],
+ ["🤓", "nerd_face", 0],
+ ["🥸", "disguised_face", 0],
+ ["😎", "sunglasses", 0],
+ ["🤩", "star_struck", 0],
+ ["🤡", "clown_face", 0],
+ ["🤠", "cowboy_hat_face", 0],
+ ["🤗", "hugs", 0],
+ ["😏", "smirk", 0],
+ ["😶", "no_mouth", 0],
+ ["😐", "neutral_face", 0],
+ ["😑", "expressionless", 0],
+ ["😒", "unamused", 0],
+ ["🙄", "roll_eyes", 0],
+ ["🤔", "thinking", 0],
+ ["🤥", "lying_face", 0],
+ ["🤭", "hand_over_mouth", 0],
+ ["🤫", "shushing", 0],
+ ["🤬", "symbols_over_mouth", 0],
+ ["🤯", "exploding_head", 0],
+ ["😳", "flushed", 0],
+ ["😞", "disappointed", 0],
+ ["😟", "worried", 0],
+ ["😠", "angry", 0],
+ ["😡", "rage", 0],
+ ["😔", "pensive", 0],
+ ["😕", "confused", 0],
+ ["🙁", "slightly_frowning_face", 0],
+ ["☹", "frowning_face", 0],
+ ["😣", "persevere", 0],
+ ["😖", "confounded", 0],
+ ["😫", "tired_face", 0],
+ ["😩", "weary", 0],
+ ["🥺", "pleading", 0],
+ ["😤", "triumph", 0],
+ ["😮", "open_mouth", 0],
+ ["😱", "scream", 0],
+ ["😨", "fearful", 0],
+ ["😰", "cold_sweat", 0],
+ ["😯", "hushed", 0],
+ ["😦", "frowning", 0],
+ ["😧", "anguished", 0],
+ ["😢", "cry", 0],
+ ["😥", "disappointed_relieved", 0],
+ ["🤤", "drooling_face", 0],
+ ["😪", "sleepy", 0],
+ ["😓", "sweat", 0],
+ ["🥵", "hot", 0],
+ ["🥶", "cold", 0],
+ ["😭", "sob", 0],
+ ["😵", "dizzy_face", 0],
+ ["😲", "astonished", 0],
+ ["🤐", "zipper_mouth_face", 0],
+ ["🤢", "nauseated_face", 0],
+ ["🤧", "sneezing_face", 0],
+ ["🤮", "vomiting", 0],
+ ["😷", "mask", 0],
+ ["🤒", "face_with_thermometer", 0],
+ ["🤕", "face_with_head_bandage", 0],
+ ["🥴", "woozy", 0],
+ ["🥱", "yawning", 0],
+ ["😴", "sleeping", 0],
+ ["💤", "zzz", 0],
+ ["😶‍🌫️", "face_in_clouds", 0],
+ ["😮‍💨", "face_exhaling", 0],
+ ["😵‍💫", "face_with_spiral_eyes", 0],
+ ["🫠", "melting_face", 0],
+ ["🫢", "face_with_open_eyes_and_hand_over_mouth", 0],
+ ["🫣", "face_with_peeking_eye", 0],
+ ["🫡", "saluting_face", 0],
+ ["🫥", "dotted_line_face", 0],
+ ["🫤", "face_with_diagonal_mouth", 0],
+ ["🥹", "face_holding_back_tears", 0],
+ ["🫨", "shaking_face", 0],
+ ["💩", "poop", 0],
+ ["😈", "smiling_imp", 0],
+ ["👿", "imp", 0],
+ ["👹", "japanese_ogre", 0],
+ ["👺", "japanese_goblin", 0],
+ ["💀", "skull", 0],
+ ["👻", "ghost", 0],
+ ["👽", "alien", 0],
+ ["🤖", "robot", 0],
+ ["😺", "smiley_cat", 0],
+ ["😸", "smile_cat", 0],
+ ["😹", "joy_cat", 0],
+ ["😻", "heart_eyes_cat", 0],
+ ["😼", "smirk_cat", 0],
+ ["😽", "kissing_cat", 0],
+ ["🙀", "scream_cat", 0],
+ ["😿", "crying_cat_face", 0],
+ ["😾", "pouting_cat", 0],
+ ["🤲", "palms_up", 1],
+ ["🙌", "raised_hands", 1],
+ ["👏", "clap", 1],
+ ["👋", "wave", 1],
+ ["🤙", "call_me_hand", 1],
+ ["👍", "+1", 1],
+ ["👎", "-1", 1],
+ ["👊", "facepunch", 1],
+ ["✊", "fist", 1],
+ ["🤛", "fist_left", 1],
+ ["🤜", "fist_right", 1],
+ ["🫷", "leftwards_pushing_hand", 1],
+ ["🫸", "rightwards_pushing_hand", 1],
+ ["✌", "v", 1],
+ ["👌", "ok_hand", 1],
+ ["✋", "raised_hand", 1],
+ ["🤚", "raised_back_of_hand", 1],
+ ["👐", "open_hands", 1],
+ ["💪", "muscle", 1],
+ ["🦾", "mechanical_arm", 1],
+ ["🙏", "pray", 1],
+ ["🦶", "foot", 1],
+ ["🦵", "leg", 1],
+ ["🦿", "mechanical_leg", 1],
+ ["🤝", "handshake", 1],
+ ["☝", "point_up", 1],
+ ["👆", "point_up_2", 1],
+ ["👇", "point_down", 1],
+ ["👈", "point_left", 1],
+ ["👉", "point_right", 1],
+ ["🖕", "fu", 1],
+ ["🖐", "raised_hand_with_fingers_splayed", 1],
+ ["🤟", "love_you", 1],
+ ["🤘", "metal", 1],
+ ["🤞", "crossed_fingers", 1],
+ ["🖖", "vulcan_salute", 1],
+ ["✍", "writing_hand", 1],
+ ["🫰", "hand_with_index_finger_and_thumb_crossed", 1],
+ ["🫱", "rightwards_hand", 1],
+ ["🫲", "leftwards_hand", 1],
+ ["🫳", "palm_down_hand", 1],
+ ["🫴", "palm_up_hand", 1],
+ ["🫵", "index_pointing_at_the_viewer", 1],
+ ["🫶", "heart_hands", 1],
+ ["🤏", "pinching_hand", 1],
+ ["🤌", "pinched_fingers", 1],
+ ["🤳", "selfie", 1],
+ ["💅", "nail_care", 1],
+ ["👄", "lips", 1],
+ ["🫦", "biting_lip", 1],
+ ["🦷", "tooth", 1],
+ ["👅", "tongue", 1],
+ ["👂", "ear", 1],
+ ["🦻", "ear_with_hearing_aid", 1],
+ ["👃", "nose", 1],
+ ["👁", "eye", 1],
+ ["👀", "eyes", 1],
+ ["🧠", "brain", 1],
+ ["🫀", "anatomical_heart", 1],
+ ["🫁", "lungs", 1],
+ ["👤", "bust_in_silhouette", 1],
+ ["👥", "busts_in_silhouette", 1],
+ ["🗣", "speaking_head", 1],
+ ["👶", "baby", 1],
+ ["🧒", "child", 1],
+ ["👦", "boy", 1],
+ ["👧", "girl", 1],
+ ["🧑", "adult", 1],
+ ["👨", "man", 1],
+ ["👩", "woman", 1],
+ ["🧑‍🦱", "curly_hair", 1],
+ ["👩‍🦱", "curly_hair_woman", 1],
+ ["👨‍🦱", "curly_hair_man", 1],
+ ["🧑‍🦰", "red_hair", 1],
+ ["👩‍🦰", "red_hair_woman", 1],
+ ["👨‍🦰", "red_hair_man", 1],
+ ["👱‍♀️", "blonde_woman", 1],
+ ["👱", "blonde_man", 1],
+ ["🧑‍🦳", "white_hair", 1],
+ ["👩‍🦳", "white_hair_woman", 1],
+ ["👨‍🦳", "white_hair_man", 1],
+ ["🧑‍🦲", "bald", 1],
+ ["👩‍🦲", "bald_woman", 1],
+ ["👨‍🦲", "bald_man", 1],
+ ["🧔", "bearded_person", 1],
+ ["🧓", "older_adult", 1],
+ ["👴", "older_man", 1],
+ ["👵", "older_woman", 1],
+ ["👲", "man_with_gua_pi_mao", 1],
+ ["🧕", "woman_with_headscarf", 1],
+ ["👳‍♀️", "woman_with_turban", 1],
+ ["👳", "man_with_turban", 1],
+ ["👮‍♀️", "policewoman", 1],
+ ["👮", "policeman", 1],
+ ["👷‍♀️", "construction_worker_woman", 1],
+ ["👷", "construction_worker_man", 1],
+ ["💂‍♀️", "guardswoman", 1],
+ ["💂", "guardsman", 1],
+ ["🕵️‍♀️", "female_detective", 1],
+ ["🕵", "male_detective", 1],
+ ["🧑‍⚕️", "health_worker", 1],
+ ["👩‍⚕️", "woman_health_worker", 1],
+ ["👨‍⚕️", "man_health_worker", 1],
+ ["🧑‍🌾", "farmer", 1],
+ ["👩‍🌾", "woman_farmer", 1],
+ ["👨‍🌾", "man_farmer", 1],
+ ["🧑‍🍳", "cook", 1],
+ ["👩‍🍳", "woman_cook", 1],
+ ["👨‍🍳", "man_cook", 1],
+ ["🧑‍🎓", "student", 1],
+ ["👩‍🎓", "woman_student", 1],
+ ["👨‍🎓", "man_student", 1],
+ ["🧑‍🎤", "singer", 1],
+ ["👩‍🎤", "woman_singer", 1],
+ ["👨‍🎤", "man_singer", 1],
+ ["🧑‍🏫", "teacher", 1],
+ ["👩‍🏫", "woman_teacher", 1],
+ ["👨‍🏫", "man_teacher", 1],
+ ["🧑‍🏭", "factory_worker", 1],
+ ["👩‍🏭", "woman_factory_worker", 1],
+ ["👨‍🏭", "man_factory_worker", 1],
+ ["🧑‍💻", "technologist", 1],
+ ["👩‍💻", "woman_technologist", 1],
+ ["👨‍💻", "man_technologist", 1],
+ ["🧑‍💼", "office_worker", 1],
+ ["👩‍💼", "woman_office_worker", 1],
+ ["👨‍💼", "man_office_worker", 1],
+ ["🧑‍🔧", "mechanic", 1],
+ ["👩‍🔧", "woman_mechanic", 1],
+ ["👨‍🔧", "man_mechanic", 1],
+ ["🧑‍🔬", "scientist", 1],
+ ["👩‍🔬", "woman_scientist", 1],
+ ["👨‍🔬", "man_scientist", 1],
+ ["🧑‍🎨", "artist", 1],
+ ["👩‍🎨", "woman_artist", 1],
+ ["👨‍🎨", "man_artist", 1],
+ ["🧑‍🚒", "firefighter", 1],
+ ["👩‍🚒", "woman_firefighter", 1],
+ ["👨‍🚒", "man_firefighter", 1],
+ ["🧑‍✈️", "pilot", 1],
+ ["👩‍✈️", "woman_pilot", 1],
+ ["👨‍✈️", "man_pilot", 1],
+ ["🧑‍🚀", "astronaut", 1],
+ ["👩‍🚀", "woman_astronaut", 1],
+ ["👨‍🚀", "man_astronaut", 1],
+ ["🧑‍⚖️", "judge", 1],
+ ["👩‍⚖️", "woman_judge", 1],
+ ["👨‍⚖️", "man_judge", 1],
+ ["🦸‍♀️", "woman_superhero", 1],
+ ["🦸‍♂️", "man_superhero", 1],
+ ["🦹‍♀️", "woman_supervillain", 1],
+ ["🦹‍♂️", "man_supervillain", 1],
+ ["🤶", "mrs_claus", 1],
+ ["🧑‍🎄", "mx_claus", 1],
+ ["🎅", "santa", 1],
+ ["🥷", "ninja", 1],
+ ["🧙‍♀️", "sorceress", 1],
+ ["🧙‍♂️", "wizard", 1],
+ ["🧝‍♀️", "woman_elf", 1],
+ ["🧝‍♂️", "man_elf", 1],
+ ["🧛‍♀️", "woman_vampire", 1],
+ ["🧛‍♂️", "man_vampire", 1],
+ ["🧟‍♀️", "woman_zombie", 1],
+ ["🧟‍♂️", "man_zombie", 1],
+ ["🧞‍♀️", "woman_genie", 1],
+ ["🧞‍♂️", "man_genie", 1],
+ ["🧜‍♀️", "mermaid", 1],
+ ["🧜‍♂️", "merman", 1],
+ ["🧚‍♀️", "woman_fairy", 1],
+ ["🧚‍♂️", "man_fairy", 1],
+ ["👼", "angel", 1],
+ ["🧌", "troll", 1],
+ ["🤰", "pregnant_woman", 1],
+ ["🫃", "pregnant_man", 1],
+ ["🫄", "pregnant_person", 1],
+ ["🫅", "person_with_crown", 1],
+ ["🤱", "breastfeeding", 1],
+ ["👩‍🍼", "woman_feeding_baby", 1],
+ ["👨‍🍼", "man_feeding_baby", 1],
+ ["🧑‍🍼", "person_feeding_baby", 1],
+ ["👸", "princess", 1],
+ ["🤴", "prince", 1],
+ ["👰", "person_with_veil", 1],
+ ["👰", "bride_with_veil", 1],
+ ["🤵", "person_in_tuxedo", 1],
+ ["🤵", "man_in_tuxedo", 1],
+ ["🏃‍♀️", "running_woman", 1],
+ ["🏃", "running_man", 1],
+ ["🚶‍♀️", "walking_woman", 1],
+ ["🚶", "walking_man", 1],
+ ["💃", "dancer", 1],
+ ["🕺", "man_dancing", 1],
+ ["👯", "dancing_women", 1],
+ ["👯‍♂️", "dancing_men", 1],
+ ["👫", "couple", 1],
+ ["🧑‍🤝‍🧑", "people_holding_hands", 1],
+ ["👬", "two_men_holding_hands", 1],
+ ["👭", "two_women_holding_hands", 1],
+ ["🫂", "people_hugging", 1],
+ ["🙇‍♀️", "bowing_woman", 1],
+ ["🙇", "bowing_man", 1],
+ ["🤦‍♂️", "man_facepalming", 1],
+ ["🤦‍♀️", "woman_facepalming", 1],
+ ["🤷", "woman_shrugging", 1],
+ ["🤷‍♂️", "man_shrugging", 1],
+ ["💁", "tipping_hand_woman", 1],
+ ["💁‍♂️", "tipping_hand_man", 1],
+ ["🙅", "no_good_woman", 1],
+ ["🙅‍♂️", "no_good_man", 1],
+ ["🙆", "ok_woman", 1],
+ ["🙆‍♂️", "ok_man", 1],
+ ["🙋", "raising_hand_woman", 1],
+ ["🙋‍♂️", "raising_hand_man", 1],
+ ["🙎", "pouting_woman", 1],
+ ["🙎‍♂️", "pouting_man", 1],
+ ["🙍", "frowning_woman", 1],
+ ["🙍‍♂️", "frowning_man", 1],
+ ["💇", "haircut_woman", 1],
+ ["💇‍♂️", "haircut_man", 1],
+ ["💆", "massage_woman", 1],
+ ["💆‍♂️", "massage_man", 1],
+ ["🧖‍♀️", "woman_in_steamy_room", 1],
+ ["🧖‍♂️", "man_in_steamy_room", 1],
+ ["🧏‍♀️", "woman_deaf", 1],
+ ["🧏‍♂️", "man_deaf", 1],
+ ["🧍‍♀️", "woman_standing", 1],
+ ["🧍‍♂️", "man_standing", 1],
+ ["🧎‍♀️", "woman_kneeling", 1],
+ ["🧎‍♂️", "man_kneeling", 1],
+ ["🧑‍🦯", "person_with_probing_cane", 1],
+ ["👩‍🦯", "woman_with_probing_cane", 1],
+ ["👨‍🦯", "man_with_probing_cane", 1],
+ ["🧑‍🦼", "person_in_motorized_wheelchair", 1],
+ ["👩‍🦼", "woman_in_motorized_wheelchair", 1],
+ ["👨‍🦼", "man_in_motorized_wheelchair", 1],
+ ["🧑‍🦽", "person_in_manual_wheelchair", 1],
+ ["👩‍🦽", "woman_in_manual_wheelchair", 1],
+ ["👨‍🦽", "man_in_manual_wheelchair", 1],
+ ["💑", "couple_with_heart_woman_man", 1],
+ ["👩‍❤️‍👩", "couple_with_heart_woman_woman", 1],
+ ["👨‍❤️‍👨", "couple_with_heart_man_man", 1],
+ ["💏", "couplekiss_man_woman", 1],
+ ["👩‍❤️‍💋‍👩", "couplekiss_woman_woman", 1],
+ ["👨‍❤️‍💋‍👨", "couplekiss_man_man", 1],
+ ["👪", "family_man_woman_boy", 1],
+ ["👨‍👩‍👧", "family_man_woman_girl", 1],
+ ["👨‍👩‍👧‍👦", "family_man_woman_girl_boy", 1],
+ ["👨‍👩‍👦‍👦", "family_man_woman_boy_boy", 1],
+ ["👨‍👩‍👧‍👧", "family_man_woman_girl_girl", 1],
+ ["👩‍👩‍👦", "family_woman_woman_boy", 1],
+ ["👩‍👩‍👧", "family_woman_woman_girl", 1],
+ ["👩‍👩‍👧‍👦", "family_woman_woman_girl_boy", 1],
+ ["👩‍👩‍👦‍👦", "family_woman_woman_boy_boy", 1],
+ ["👩‍👩‍👧‍👧", "family_woman_woman_girl_girl", 1],
+ ["👨‍👨‍👦", "family_man_man_boy", 1],
+ ["👨‍👨‍👧", "family_man_man_girl", 1],
+ ["👨‍👨‍👧‍👦", "family_man_man_girl_boy", 1],
+ ["👨‍👨‍👦‍👦", "family_man_man_boy_boy", 1],
+ ["👨‍👨‍👧‍👧", "family_man_man_girl_girl", 1],
+ ["👩‍👦", "family_woman_boy", 1],
+ ["👩‍👧", "family_woman_girl", 1],
+ ["👩‍👧‍👦", "family_woman_girl_boy", 1],
+ ["👩‍👦‍👦", "family_woman_boy_boy", 1],
+ ["👩‍👧‍👧", "family_woman_girl_girl", 1],
+ ["👨‍👦", "family_man_boy", 1],
+ ["👨‍👧", "family_man_girl", 1],
+ ["👨‍👧‍👦", "family_man_girl_boy", 1],
+ ["👨‍👦‍👦", "family_man_boy_boy", 1],
+ ["👨‍👧‍👧", "family_man_girl_girl", 1],
+ ["🧶", "yarn", 1],
+ ["🧵", "thread", 1],
+ ["🧥", "coat", 1],
+ ["🥼", "labcoat", 1],
+ ["👚", "womans_clothes", 1],
+ ["👕", "tshirt", 1],
+ ["👖", "jeans", 1],
+ ["👔", "necktie", 1],
+ ["👗", "dress", 1],
+ ["👙", "bikini", 1],
+ ["🩱", "one_piece_swimsuit", 1],
+ ["👘", "kimono", 1],
+ ["🥻", "sari", 1],
+ ["🩲", "briefs", 1],
+ ["🩳", "shorts", 1],
+ ["💄", "lipstick", 1],
+ ["💋", "kiss", 1],
+ ["👣", "footprints", 1],
+ ["🥿", "flat_shoe", 1],
+ ["👠", "high_heel", 1],
+ ["👡", "sandal", 1],
+ ["👢", "boot", 1],
+ ["👞", "mans_shoe", 1],
+ ["👟", "athletic_shoe", 1],
+ ["🩴", "thong_sandal", 1],
+ ["🩰", "ballet_shoes", 1],
+ ["🧦", "socks", 1],
+ ["🧤", "gloves", 1],
+ ["🧣", "scarf", 1],
+ ["👒", "womans_hat", 1],
+ ["🎩", "tophat", 1],
+ ["🧢", "billed_hat", 1],
+ ["⛑", "rescue_worker_helmet", 1],
+ ["🪖", "military_helmet", 1],
+ ["🎓", "mortar_board", 1],
+ ["👑", "crown", 1],
+ ["🎒", "school_satchel", 1],
+ ["🧳", "luggage", 1],
+ ["👝", "pouch", 1],
+ ["👛", "purse", 1],
+ ["👜", "handbag", 1],
+ ["💼", "briefcase", 1],
+ ["👓", "eyeglasses", 1],
+ ["🕶", "dark_sunglasses", 1],
+ ["🥽", "goggles", 1],
+ ["💍", "ring", 1],
+ ["🌂", "closed_umbrella", 1],
+ ["🐶", "dog", 2],
+ ["🐱", "cat", 2],
+ ["🐈‍⬛", "black_cat", 2],
+ ["🐭", "mouse", 2],
+ ["🐹", "hamster", 2],
+ ["🐰", "rabbit", 2],
+ ["🦊", "fox_face", 2],
+ ["🐻", "bear", 2],
+ ["🐼", "panda_face", 2],
+ ["🐨", "koala", 2],
+ ["🐯", "tiger", 2],
+ ["🦁", "lion", 2],
+ ["🐮", "cow", 2],
+ ["🐷", "pig", 2],
+ ["🐽", "pig_nose", 2],
+ ["🐸", "frog", 2],
+ ["🦑", "squid", 2],
+ ["🐙", "octopus", 2],
+ ["🪼", "jellyfish", 2],
+ ["🦐", "shrimp", 2],
+ ["🐵", "monkey_face", 2],
+ ["🦍", "gorilla", 2],
+ ["🙈", "see_no_evil", 2],
+ ["🙉", "hear_no_evil", 2],
+ ["🙊", "speak_no_evil", 2],
+ ["🐒", "monkey", 2],
+ ["🐔", "chicken", 2],
+ ["🐧", "penguin", 2],
+ ["🐦", "bird", 2],
+ ["🐤", "baby_chick", 2],
+ ["🐣", "hatching_chick", 2],
+ ["🐥", "hatched_chick", 2],
+ ["🪿", "goose", 2],
+ ["🦆", "duck", 2],
+ ["🐦‍⬛", "black_bird", 2],
+ ["🦅", "eagle", 2],
+ ["🦉", "owl", 2],
+ ["🦇", "bat", 2],
+ ["🐺", "wolf", 2],
+ ["🐗", "boar", 2],
+ ["🐴", "horse", 2],
+ ["🦄", "unicorn", 2],
+ ["🫎", "moose", 2],
+ ["🐝", "honeybee", 2],
+ ["🐛", "bug", 2],
+ ["🦋", "butterfly", 2],
+ ["🐌", "snail", 2],
+ ["🐞", "lady_beetle", 2],
+ ["🐜", "ant", 2],
+ ["🦗", "grasshopper", 2],
+ ["🕷", "spider", 2],
+ ["🪲", "beetle", 2],
+ ["🪳", "cockroach", 2],
+ ["🪰", "fly", 2],
+ ["🪱", "worm", 2],
+ ["🦂", "scorpion", 2],
+ ["🦀", "crab", 2],
+ ["🐍", "snake", 2],
+ ["🦎", "lizard", 2],
+ ["🦖", "t-rex", 2],
+ ["🦕", "sauropod", 2],
+ ["🐢", "turtle", 2],
+ ["🐠", "tropical_fish", 2],
+ ["🐟", "fish", 2],
+ ["🐡", "blowfish", 2],
+ ["🐬", "dolphin", 2],
+ ["🦈", "shark", 2],
+ ["🐳", "whale", 2],
+ ["🐋", "whale2", 2],
+ ["🐊", "crocodile", 2],
+ ["🐆", "leopard", 2],
+ ["🦓", "zebra", 2],
+ ["🐅", "tiger2", 2],
+ ["🐃", "water_buffalo", 2],
+ ["🐂", "ox", 2],
+ ["🐄", "cow2", 2],
+ ["🦌", "deer", 2],
+ ["🐪", "dromedary_camel", 2],
+ ["🐫", "camel", 2],
+ ["🦒", "giraffe", 2],
+ ["🐘", "elephant", 2],
+ ["🦏", "rhinoceros", 2],
+ ["🐐", "goat", 2],
+ ["🐏", "ram", 2],
+ ["🐑", "sheep", 2],
+ ["🫏", "donkey", 2],
+ ["🐎", "racehorse", 2],
+ ["🐖", "pig2", 2],
+ ["🐀", "rat", 2],
+ ["🐁", "mouse2", 2],
+ ["🐓", "rooster", 2],
+ ["🦃", "turkey", 2],
+ ["🕊", "dove", 2],
+ ["🐕", "dog2", 2],
+ ["🐩", "poodle", 2],
+ ["🐈", "cat2", 2],
+ ["🐇", "rabbit2", 2],
+ ["🐿", "chipmunk", 2],
+ ["🦔", "hedgehog", 2],
+ ["🦝", "raccoon", 2],
+ ["🦙", "llama", 2],
+ ["🦛", "hippopotamus", 2],
+ ["🦘", "kangaroo", 2],
+ ["🦡", "badger", 2],
+ ["🦢", "swan", 2],
+ ["🦚", "peacock", 2],
+ ["🦜", "parrot", 2],
+ ["🦞", "lobster", 2],
+ ["🦠", "microbe", 2],
+ ["🦟", "mosquito", 2],
+ ["🦬", "bison", 2],
+ ["🦣", "mammoth", 2],
+ ["🦫", "beaver", 2],
+ ["🐻‍❄️", "polar_bear", 2],
+ ["🦤", "dodo", 2],
+ ["🪶", "feather", 2],
+ ["🪽", "wing", 2],
+ ["🦭", "seal", 2],
+ ["🐾", "paw_prints", 2],
+ ["🐉", "dragon", 2],
+ ["🐲", "dragon_face", 2],
+ ["🦧", "orangutan", 2],
+ ["🦮", "guide_dog", 2],
+ ["🐕‍🦺", "service_dog", 2],
+ ["🦥", "sloth", 2],
+ ["🦦", "otter", 2],
+ ["🦨", "skunk", 2],
+ ["🦩", "flamingo", 2],
+ ["🌵", "cactus", 2],
+ ["🎄", "christmas_tree", 2],
+ ["🌲", "evergreen_tree", 2],
+ ["🌳", "deciduous_tree", 2],
+ ["🌴", "palm_tree", 2],
+ ["🌱", "seedling", 2],
+ ["🌿", "herb", 2],
+ ["☘", "shamrock", 2],
+ ["🍀", "four_leaf_clover", 2],
+ ["🎍", "bamboo", 2],
+ ["🎋", "tanabata_tree", 2],
+ ["🍃", "leaves", 2],
+ ["🍂", "fallen_leaf", 2],
+ ["🍁", "maple_leaf", 2],
+ ["🌾", "ear_of_rice", 2],
+ ["🌺", "hibiscus", 2],
+ ["🌻", "sunflower", 2],
+ ["🌹", "rose", 2],
+ ["🥀", "wilted_flower", 2],
+ ["🪻", "hyacinth", 2],
+ ["🌷", "tulip", 2],
+ ["🌼", "blossom", 2],
+ ["🌸", "cherry_blossom", 2],
+ ["💐", "bouquet", 2],
+ ["🍄", "mushroom", 2],
+ ["🪴", "potted_plant", 2],
+ ["🌰", "chestnut", 2],
+ ["🎃", "jack_o_lantern", 2],
+ ["🐚", "shell", 2],
+ ["🕸", "spider_web", 2],
+ ["🌎", "earth_americas", 2],
+ ["🌍", "earth_africa", 2],
+ ["🌏", "earth_asia", 2],
+ ["🪐", "ringed_planet", 2],
+ ["🌕", "full_moon", 2],
+ ["🌖", "waning_gibbous_moon", 2],
+ ["🌗", "last_quarter_moon", 2],
+ ["🌘", "waning_crescent_moon", 2],
+ ["🌑", "new_moon", 2],
+ ["🌒", "waxing_crescent_moon", 2],
+ ["🌓", "first_quarter_moon", 2],
+ ["🌔", "waxing_gibbous_moon", 2],
+ ["🌚", "new_moon_with_face", 2],
+ ["🌝", "full_moon_with_face", 2],
+ ["🌛", "first_quarter_moon_with_face", 2],
+ ["🌜", "last_quarter_moon_with_face", 2],
+ ["🌞", "sun_with_face", 2],
+ ["🌙", "crescent_moon", 2],
+ ["⭐", "star", 2],
+ ["🌟", "star2", 2],
+ ["💫", "dizzy", 2],
+ ["✨", "sparkles", 2],
+ ["☄", "comet", 2],
+ ["☀️", "sunny", 2],
+ ["🌤", "sun_behind_small_cloud", 2],
+ ["⛅", "partly_sunny", 2],
+ ["🌥", "sun_behind_large_cloud", 2],
+ ["🌦", "sun_behind_rain_cloud", 2],
+ ["☁️", "cloud", 2],
+ ["🌧", "cloud_with_rain", 2],
+ ["⛈", "cloud_with_lightning_and_rain", 2],
+ ["🌩", "cloud_with_lightning", 2],
+ ["⚡", "zap", 2],
+ ["🔥", "fire", 2],
+ ["💥", "boom", 2],
+ ["❄️", "snowflake", 2],
+ ["🌨", "cloud_with_snow", 2],
+ ["⛄", "snowman", 2],
+ ["☃", "snowman_with_snow", 2],
+ ["🌬", "wind_face", 2],
+ ["💨", "dash", 2],
+ ["🌪", "tornado", 2],
+ ["🌫", "fog", 2],
+ ["☂", "open_umbrella", 2],
+ ["☔", "umbrella", 2],
+ ["💧", "droplet", 2],
+ ["💦", "sweat_drops", 2],
+ ["🌊", "ocean", 2],
+ ["🪷", "lotus", 2],
+ ["🪸", "coral", 2],
+ ["🪹", "empty_nest", 2],
+ ["🪺", "nest_with_eggs", 2],
+ ["🍏", "green_apple", 3],
+ ["🍎", "apple", 3],
+ ["🍐", "pear", 3],
+ ["🍊", "tangerine", 3],
+ ["🍋", "lemon", 3],
+ ["🍌", "banana", 3],
+ ["🍉", "watermelon", 3],
+ ["🍇", "grapes", 3],
+ ["🍓", "strawberry", 3],
+ ["🍈", "melon", 3],
+ ["🍒", "cherries", 3],
+ ["🍑", "peach", 3],
+ ["🍍", "pineapple", 3],
+ ["🥥", "coconut", 3],
+ ["🥝", "kiwi_fruit", 3],
+ ["🥭", "mango", 3],
+ ["🥑", "avocado", 3],
+ ["🫛", "pea_pod", 3],
+ ["🥦", "broccoli", 3],
+ ["🍅", "tomato", 3],
+ ["🍆", "eggplant", 3],
+ ["🥒", "cucumber", 3],
+ ["🫐", "blueberries", 3],
+ ["🫒", "olive", 3],
+ ["🫑", "bell_pepper", 3],
+ ["🥕", "carrot", 3],
+ ["🌶", "hot_pepper", 3],
+ ["🥔", "potato", 3],
+ ["🌽", "corn", 3],
+ ["🥬", "leafy_greens", 3],
+ ["🍠", "sweet_potato", 3],
+ ["🫚", "ginger_root", 3],
+ ["🥜", "peanuts", 3],
+ ["🧄", "garlic", 3],
+ ["🧅", "onion", 3],
+ ["🍯", "honey_pot", 3],
+ ["🥐", "croissant", 3],
+ ["🍞", "bread", 3],
+ ["🥖", "baguette_bread", 3],
+ ["🥯", "bagel", 3],
+ ["🥨", "pretzel", 3],
+ ["🧀", "cheese", 3],
+ ["🥚", "egg", 3],
+ ["🥓", "bacon", 3],
+ ["🥩", "steak", 3],
+ ["🥞", "pancakes", 3],
+ ["🍗", "poultry_leg", 3],
+ ["🍖", "meat_on_bone", 3],
+ ["🦴", "bone", 3],
+ ["🍤", "fried_shrimp", 3],
+ ["🍳", "fried_egg", 3],
+ ["🍔", "hamburger", 3],
+ ["🍟", "fries", 3],
+ ["🥙", "stuffed_flatbread", 3],
+ ["🌭", "hotdog", 3],
+ ["🍕", "pizza", 3],
+ ["🥪", "sandwich", 3],
+ ["🥫", "canned_food", 3],
+ ["🍝", "spaghetti", 3],
+ ["🌮", "taco", 3],
+ ["🌯", "burrito", 3],
+ ["🥗", "green_salad", 3],
+ ["🥘", "shallow_pan_of_food", 3],
+ ["🍜", "ramen", 3],
+ ["🍲", "stew", 3],
+ ["🍥", "fish_cake", 3],
+ ["🥠", "fortune_cookie", 3],
+ ["🍣", "sushi", 3],
+ ["🍱", "bento", 3],
+ ["🍛", "curry", 3],
+ ["🍙", "rice_ball", 3],
+ ["🍚", "rice", 3],
+ ["🍘", "rice_cracker", 3],
+ ["🍢", "oden", 3],
+ ["🍡", "dango", 3],
+ ["🍧", "shaved_ice", 3],
+ ["🍨", "ice_cream", 3],
+ ["🍦", "icecream", 3],
+ ["🥧", "pie", 3],
+ ["🍰", "cake", 3],
+ ["🧁", "cupcake", 3],
+ ["🥮", "moon_cake", 3],
+ ["🎂", "birthday", 3],
+ ["🍮", "custard", 3],
+ ["🍬", "candy", 3],
+ ["🍭", "lollipop", 3],
+ ["🍫", "chocolate_bar", 3],
+ ["🍿", "popcorn", 3],
+ ["🥟", "dumpling", 3],
+ ["🍩", "doughnut", 3],
+ ["🍪", "cookie", 3],
+ ["🧇", "waffle", 3],
+ ["🧆", "falafel", 3],
+ ["🧈", "butter", 3],
+ ["🦪", "oyster", 3],
+ ["🫓", "flatbread", 3],
+ ["🫔", "tamale", 3],
+ ["🫕", "fondue", 3],
+ ["🥛", "milk_glass", 3],
+ ["🍺", "beer", 3],
+ ["🍻", "beers", 3],
+ ["🥂", "clinking_glasses", 3],
+ ["🍷", "wine_glass", 3],
+ ["🥃", "tumbler_glass", 3],
+ ["🍸", "cocktail", 3],
+ ["🍹", "tropical_drink", 3],
+ ["🍾", "champagne", 3],
+ ["🍶", "sake", 3],
+ ["🍵", "tea", 3],
+ ["🥤", "cup_with_straw", 3],
+ ["☕", "coffee", 3],
+ ["🫖", "teapot", 3],
+ ["🧋", "bubble_tea", 3],
+ ["🍼", "baby_bottle", 3],
+ ["🧃", "beverage_box", 3],
+ ["🧉", "mate", 3],
+ ["🧊", "ice_cube", 3],
+ ["🧂", "salt", 3],
+ ["🥄", "spoon", 3],
+ ["🍴", "fork_and_knife", 3],
+ ["🍽", "plate_with_cutlery", 3],
+ ["🥣", "bowl_with_spoon", 3],
+ ["🥡", "takeout_box", 3],
+ ["🥢", "chopsticks", 3],
+ ["🫗", "pouring_liquid", 3],
+ ["🫘", "beans", 3],
+ ["🫙", "jar", 3],
+ ["⚽", "soccer", 4],
+ ["🏀", "basketball", 4],
+ ["🏈", "football", 4],
+ ["⚾", "baseball", 4],
+ ["🥎", "softball", 4],
+ ["🎾", "tennis", 4],
+ ["🏐", "volleyball", 4],
+ ["🏉", "rugby_football", 4],
+ ["🥏", "flying_disc", 4],
+ ["🎱", "8ball", 4],
+ ["⛳", "golf", 4],
+ ["🏌️‍♀️", "golfing_woman", 4],
+ ["🏌", "golfing_man", 4],
+ ["🏓", "ping_pong", 4],
+ ["🏸", "badminton", 4],
+ ["🥅", "goal_net", 4],
+ ["🏒", "ice_hockey", 4],
+ ["🏑", "field_hockey", 4],
+ ["🥍", "lacrosse", 4],
+ ["🏏", "cricket", 4],
+ ["🎿", "ski", 4],
+ ["⛷", "skier", 4],
+ ["🏂", "snowboarder", 4],
+ ["🤺", "person_fencing", 4],
+ ["🤼‍♀️", "women_wrestling", 4],
+ ["🤼‍♂️", "men_wrestling", 4],
+ ["🤸‍♀️", "woman_cartwheeling", 4],
+ ["🤸‍♂️", "man_cartwheeling", 4],
+ ["🤾‍♀️", "woman_playing_handball", 4],
+ ["🤾‍♂️", "man_playing_handball", 4],
+ ["⛸", "ice_skate", 4],
+ ["🥌", "curling_stone", 4],
+ ["🛹", "skateboard", 4],
+ ["🛷", "sled", 4],
+ ["🏹", "bow_and_arrow", 4],
+ ["🎣", "fishing_pole_and_fish", 4],
+ ["🥊", "boxing_glove", 4],
+ ["🥋", "martial_arts_uniform", 4],
+ ["🚣‍♀️", "rowing_woman", 4],
+ ["🚣", "rowing_man", 4],
+ ["🧗‍♀️", "climbing_woman", 4],
+ ["🧗‍♂️", "climbing_man", 4],
+ ["🏊‍♀️", "swimming_woman", 4],
+ ["🏊", "swimming_man", 4],
+ ["🤽‍♀️", "woman_playing_water_polo", 4],
+ ["🤽‍♂️", "man_playing_water_polo", 4],
+ ["🧘‍♀️", "woman_in_lotus_position", 4],
+ ["🧘‍♂️", "man_in_lotus_position", 4],
+ ["🏄‍♀️", "surfing_woman", 4],
+ ["🏄", "surfing_man", 4],
+ ["🛀", "bath", 4],
+ ["⛹️‍♀️", "basketball_woman", 4],
+ ["⛹", "basketball_man", 4],
+ ["🏋️‍♀️", "weight_lifting_woman", 4],
+ ["🏋", "weight_lifting_man", 4],
+ ["🚴‍♀️", "biking_woman", 4],
+ ["🚴", "biking_man", 4],
+ ["🚵‍♀️", "mountain_biking_woman", 4],
+ ["🚵", "mountain_biking_man", 4],
+ ["🏇", "horse_racing", 4],
+ ["🤿", "diving_mask", 4],
+ ["🪀", "yo_yo", 4],
+ ["🪁", "kite", 4],
+ ["🦺", "safety_vest", 4],
+ ["🪡", "sewing_needle", 4],
+ ["🪢", "knot", 4],
+ ["🕴", "business_suit_levitating", 4],
+ ["🏆", "trophy", 4],
+ ["🎽", "running_shirt_with_sash", 4],
+ ["🏅", "medal_sports", 4],
+ ["🎖", "medal_military", 4],
+ ["🥇", "1st_place_medal", 4],
+ ["🥈", "2nd_place_medal", 4],
+ ["🥉", "3rd_place_medal", 4],
+ ["🎗", "reminder_ribbon", 4],
+ ["🏵", "rosette", 4],
+ ["🎫", "ticket", 4],
+ ["🎟", "tickets", 4],
+ ["🎭", "performing_arts", 4],
+ ["🎨", "art", 4],
+ ["🎪", "circus_tent", 4],
+ ["🤹‍♀️", "woman_juggling", 4],
+ ["🤹‍♂️", "man_juggling", 4],
+ ["🎤", "microphone", 4],
+ ["🎧", "headphones", 4],
+ ["🎼", "musical_score", 4],
+ ["🎹", "musical_keyboard", 4],
+ ["🪇", "maracas", 4],
+ ["🥁", "drum", 4],
+ ["🎷", "saxophone", 4],
+ ["🎺", "trumpet", 4],
+ ["🪈", "flute", 4],
+ ["🎸", "guitar", 4],
+ ["🎻", "violin", 4],
+ ["🪕", "banjo", 4],
+ ["🪗", "accordion", 4],
+ ["🪘", "long_drum", 4],
+ ["🎬", "clapper", 4],
+ ["🎮", "video_game", 4],
+ ["👾", "space_invader", 4],
+ ["🎯", "dart", 4],
+ ["🎲", "game_die", 4],
+ ["♟️", "chess_pawn", 4],
+ ["🎰", "slot_machine", 4],
+ ["🧩", "jigsaw", 4],
+ ["🎳", "bowling", 4],
+ ["🪄", "magic_wand", 4],
+ ["🪅", "pinata", 4],
+ ["🪆", "nesting_dolls", 4],
+ ["🪬", "hamsa", 4],
+ ["🪩", "mirror_ball", 4],
+ ["🚗", "red_car", 5],
+ ["🚕", "taxi", 5],
+ ["🚙", "blue_car", 5],
+ ["🚌", "bus", 5],
+ ["🚎", "trolleybus", 5],
+ ["🏎", "racing_car", 5],
+ ["🚓", "police_car", 5],
+ ["🚑", "ambulance", 5],
+ ["🚒", "fire_engine", 5],
+ ["🚐", "minibus", 5],
+ ["🚚", "truck", 5],
+ ["🚛", "articulated_lorry", 5],
+ ["🚜", "tractor", 5],
+ ["🛴", "kick_scooter", 5],
+ ["🏍", "motorcycle", 5],
+ ["🚲", "bike", 5],
+ ["🛵", "motor_scooter", 5],
+ ["🦽", "manual_wheelchair", 5],
+ ["🦼", "motorized_wheelchair", 5],
+ ["🛺", "auto_rickshaw", 5],
+ ["🪂", "parachute", 5],
+ ["🚨", "rotating_light", 5],
+ ["🚔", "oncoming_police_car", 5],
+ ["🚍", "oncoming_bus", 5],
+ ["🚘", "oncoming_automobile", 5],
+ ["🚖", "oncoming_taxi", 5],
+ ["🚡", "aerial_tramway", 5],
+ ["🚠", "mountain_cableway", 5],
+ ["🚟", "suspension_railway", 5],
+ ["🚃", "railway_car", 5],
+ ["🚋", "train", 5],
+ ["🚝", "monorail", 5],
+ ["🚄", "bullettrain_side", 5],
+ ["🚅", "bullettrain_front", 5],
+ ["🚈", "light_rail", 5],
+ ["🚞", "mountain_railway", 5],
+ ["🚂", "steam_locomotive", 5],
+ ["🚆", "train2", 5],
+ ["🚇", "metro", 5],
+ ["🚊", "tram", 5],
+ ["🚉", "station", 5],
+ ["🛸", "flying_saucer", 5],
+ ["🚁", "helicopter", 5],
+ ["🛩", "small_airplane", 5],
+ ["✈️", "airplane", 5],
+ ["🛫", "flight_departure", 5],
+ ["🛬", "flight_arrival", 5],
+ ["⛵", "sailboat", 5],
+ ["🛥", "motor_boat", 5],
+ ["🚤", "speedboat", 5],
+ ["⛴", "ferry", 5],
+ ["🛳", "passenger_ship", 5],
+ ["🚀", "rocket", 5],
+ ["🛰", "artificial_satellite", 5],
+ ["🛻", "pickup_truck", 5],
+ ["🛼", "roller_skate", 5],
+ ["💺", "seat", 5],
+ ["🛶", "canoe", 5],
+ ["⚓", "anchor", 5],
+ ["🚧", "construction", 5],
+ ["⛽", "fuelpump", 5],
+ ["🚏", "busstop", 5],
+ ["🚦", "vertical_traffic_light", 5],
+ ["🚥", "traffic_light", 5],
+ ["🏁", "checkered_flag", 5],
+ ["🚢", "ship", 5],
+ ["🎡", "ferris_wheel", 5],
+ ["🎢", "roller_coaster", 5],
+ ["🎠", "carousel_horse", 5],
+ ["🏗", "building_construction", 5],
+ ["🌁", "foggy", 5],
+ ["🏭", "factory", 5],
+ ["⛲", "fountain", 5],
+ ["🎑", "rice_scene", 5],
+ ["⛰", "mountain", 5],
+ ["🏔", "mountain_snow", 5],
+ ["🗻", "mount_fuji", 5],
+ ["🌋", "volcano", 5],
+ ["🗾", "japan", 5],
+ ["🏕", "camping", 5],
+ ["⛺", "tent", 5],
+ ["🏞", "national_park", 5],
+ ["🛣", "motorway", 5],
+ ["🛤", "railway_track", 5],
+ ["🌅", "sunrise", 5],
+ ["🌄", "sunrise_over_mountains", 5],
+ ["🏜", "desert", 5],
+ ["🏖", "beach_umbrella", 5],
+ ["🏝", "desert_island", 5],
+ ["🌇", "city_sunrise", 5],
+ ["🌆", "city_sunset", 5],
+ ["🏙", "cityscape", 5],
+ ["🌃", "night_with_stars", 5],
+ ["🌉", "bridge_at_night", 5],
+ ["🌌", "milky_way", 5],
+ ["🌠", "stars", 5],
+ ["🎇", "sparkler", 5],
+ ["🎆", "fireworks", 5],
+ ["🌈", "rainbow", 5],
+ ["🏘", "houses", 5],
+ ["🏰", "european_castle", 5],
+ ["🏯", "japanese_castle", 5],
+ ["🗼", "tokyo_tower", 5],
+ ["", "shibuya_109", 5],
+ ["🏟", "stadium", 5],
+ ["🗽", "statue_of_liberty", 5],
+ ["🏠", "house", 5],
+ ["🏡", "house_with_garden", 5],
+ ["🏚", "derelict_house", 5],
+ ["🏢", "office", 5],
+ ["🏬", "department_store", 5],
+ ["🏣", "post_office", 5],
+ ["🏤", "european_post_office", 5],
+ ["🏥", "hospital", 5],
+ ["🏦", "bank", 5],
+ ["🏨", "hotel", 5],
+ ["🏪", "convenience_store", 5],
+ ["🏫", "school", 5],
+ ["🏩", "love_hotel", 5],
+ ["💒", "wedding", 5],
+ ["🏛", "classical_building", 5],
+ ["⛪", "church", 5],
+ ["🕌", "mosque", 5],
+ ["🕍", "synagogue", 5],
+ ["🕋", "kaaba", 5],
+ ["⛩", "shinto_shrine", 5],
+ ["🛕", "hindu_temple", 5],
+ ["🪨", "rock", 5],
+ ["🪵", "wood", 5],
+ ["🛖", "hut", 5],
+ ["🛝", "playground_slide", 5],
+ ["🛞", "wheel", 5],
+ ["🛟", "ring_buoy", 5],
+ ["⌚", "watch", 6],
+ ["📱", "iphone", 6],
+ ["📲", "calling", 6],
+ ["💻", "computer", 6],
+ ["⌨", "keyboard", 6],
+ ["🖥", "desktop_computer", 6],
+ ["🖨", "printer", 6],
+ ["🖱", "computer_mouse", 6],
+ ["🖲", "trackball", 6],
+ ["🕹", "joystick", 6],
+ ["🗜", "clamp", 6],
+ ["💽", "minidisc", 6],
+ ["💾", "floppy_disk", 6],
+ ["💿", "cd", 6],
+ ["📀", "dvd", 6],
+ ["📼", "vhs", 6],
+ ["📷", "camera", 6],
+ ["📸", "camera_flash", 6],
+ ["📹", "video_camera", 6],
+ ["🎥", "movie_camera", 6],
+ ["📽", "film_projector", 6],
+ ["🎞", "film_strip", 6],
+ ["📞", "telephone_receiver", 6],
+ ["☎️", "phone", 6],
+ ["📟", "pager", 6],
+ ["📠", "fax", 6],
+ ["📺", "tv", 6],
+ ["📻", "radio", 6],
+ ["🎙", "studio_microphone", 6],
+ ["🎚", "level_slider", 6],
+ ["🎛", "control_knobs", 6],
+ ["🧭", "compass", 6],
+ ["⏱", "stopwatch", 6],
+ ["⏲", "timer_clock", 6],
+ ["⏰", "alarm_clock", 6],
+ ["🕰", "mantelpiece_clock", 6],
+ ["⏳", "hourglass_flowing_sand", 6],
+ ["⌛", "hourglass", 6],
+ ["📡", "satellite", 6],
+ ["🔋", "battery", 6],
+ ["🪫", "low_battery", 6],
+ ["🔌", "electric_plug", 6],
+ ["💡", "bulb", 6],
+ ["🔦", "flashlight", 6],
+ ["🕯", "candle", 6],
+ ["🧯", "fire_extinguisher", 6],
+ ["🗑", "wastebasket", 6],
+ ["🛢", "oil_drum", 6],
+ ["💸", "money_with_wings", 6],
+ ["💵", "dollar", 6],
+ ["💴", "yen", 6],
+ ["💶", "euro", 6],
+ ["💷", "pound", 6],
+ ["💰", "moneybag", 6],
+ ["🪙", "coin", 6],
+ ["💳", "credit_card", 6],
+ ["🪪", "identification_card", 6],
+ ["💎", "gem", 6],
+ ["⚖", "balance_scale", 6],
+ ["🧰", "toolbox", 6],
+ ["🔧", "wrench", 6],
+ ["🔨", "hammer", 6],
+ ["⚒", "hammer_and_pick", 6],
+ ["🛠", "hammer_and_wrench", 6],
+ ["⛏", "pick", 6],
+ ["🪓", "axe", 6],
+ ["🦯", "probing_cane", 6],
+ ["🔩", "nut_and_bolt", 6],
+ ["⚙", "gear", 6],
+ ["🪃", "boomerang", 6],
+ ["🪚", "carpentry_saw", 6],
+ ["🪛", "screwdriver", 6],
+ ["🪝", "hook", 6],
+ ["🪜", "ladder", 6],
+ ["🧱", "brick", 6],
+ ["⛓", "chains", 6],
+ ["🧲", "magnet", 6],
+ ["🔫", "gun", 6],
+ ["💣", "bomb", 6],
+ ["🧨", "firecracker", 6],
+ ["🔪", "hocho", 6],
+ ["🗡", "dagger", 6],
+ ["⚔", "crossed_swords", 6],
+ ["🛡", "shield", 6],
+ ["🚬", "smoking", 6],
+ ["☠", "skull_and_crossbones", 6],
+ ["⚰", "coffin", 6],
+ ["⚱", "funeral_urn", 6],
+ ["🏺", "amphora", 6],
+ ["🔮", "crystal_ball", 6],
+ ["📿", "prayer_beads", 6],
+ ["🧿", "nazar_amulet", 6],
+ ["💈", "barber", 6],
+ ["⚗", "alembic", 6],
+ ["🔭", "telescope", 6],
+ ["🔬", "microscope", 6],
+ ["🕳", "hole", 6],
+ ["💊", "pill", 6],
+ ["💉", "syringe", 6],
+ ["🩸", "drop_of_blood", 6],
+ ["🩹", "adhesive_bandage", 6],
+ ["🩺", "stethoscope", 6],
+ ["🪒", "razor", 6],
+ ["🪮", "hair_pick", 6],
+ ["🩻", "xray", 6],
+ ["🩼", "crutch", 6],
+ ["🧬", "dna", 6],
+ ["🧫", "petri_dish", 6],
+ ["🧪", "test_tube", 6],
+ ["🌡", "thermometer", 6],
+ ["🧹", "broom", 6],
+ ["🧺", "basket", 6],
+ ["🧻", "toilet_paper", 6],
+ ["🏷", "label", 6],
+ ["🔖", "bookmark", 6],
+ ["🚽", "toilet", 6],
+ ["🚿", "shower", 6],
+ ["🛁", "bathtub", 6],
+ ["🧼", "soap", 6],
+ ["🧽", "sponge", 6],
+ ["🧴", "lotion_bottle", 6],
+ ["🔑", "key", 6],
+ ["🗝", "old_key", 6],
+ ["🛋", "couch_and_lamp", 6],
+ ["🪔", "diya_Lamp", 6],
+ ["🛌", "sleeping_bed", 6],
+ ["🛏", "bed", 6],
+ ["🚪", "door", 6],
+ ["🪑", "chair", 6],
+ ["🛎", "bellhop_bell", 6],
+ ["🧸", "teddy_bear", 6],
+ ["🖼", "framed_picture", 6],
+ ["🗺", "world_map", 6],
+ ["🛗", "elevator", 6],
+ ["🪞", "mirror", 6],
+ ["🪟", "window", 6],
+ ["🪠", "plunger", 6],
+ ["🪤", "mouse_trap", 6],
+ ["🪣", "bucket", 6],
+ ["🪥", "toothbrush", 6],
+ ["🫧", "bubbles", 6],
+ ["⛱", "parasol_on_ground", 6],
+ ["🗿", "moyai", 6],
+ ["🛍", "shopping", 6],
+ ["🛒", "shopping_cart", 6],
+ ["🎈", "balloon", 6],
+ ["🎏", "flags", 6],
+ ["🎀", "ribbon", 6],
+ ["🎁", "gift", 6],
+ ["🎊", "confetti_ball", 6],
+ ["🎉", "tada", 6],
+ ["🎎", "dolls", 6],
+ ["🪭", "folding_hand_fan", 6],
+ ["🎐", "wind_chime", 6],
+ ["🎌", "crossed_flags", 6],
+ ["🏮", "izakaya_lantern", 6],
+ ["🧧", "red_envelope", 6],
+ ["✉️", "email", 6],
+ ["📩", "envelope_with_arrow", 6],
+ ["📨", "incoming_envelope", 6],
+ ["📧", "e-mail", 6],
+ ["💌", "love_letter", 6],
+ ["📮", "postbox", 6],
+ ["📪", "mailbox_closed", 6],
+ ["📫", "mailbox", 6],
+ ["📬", "mailbox_with_mail", 6],
+ ["📭", "mailbox_with_no_mail", 6],
+ ["📦", "package", 6],
+ ["📯", "postal_horn", 6],
+ ["📥", "inbox_tray", 6],
+ ["📤", "outbox_tray", 6],
+ ["📜", "scroll", 6],
+ ["📃", "page_with_curl", 6],
+ ["📑", "bookmark_tabs", 6],
+ ["🧾", "receipt", 6],
+ ["📊", "bar_chart", 6],
+ ["📈", "chart_with_upwards_trend", 6],
+ ["📉", "chart_with_downwards_trend", 6],
+ ["📄", "page_facing_up", 6],
+ ["📅", "date", 6],
+ ["📆", "calendar", 6],
+ ["🗓", "spiral_calendar", 6],
+ ["📇", "card_index", 6],
+ ["🗃", "card_file_box", 6],
+ ["🗳", "ballot_box", 6],
+ ["🗄", "file_cabinet", 6],
+ ["📋", "clipboard", 6],
+ ["🗒", "spiral_notepad", 6],
+ ["📁", "file_folder", 6],
+ ["📂", "open_file_folder", 6],
+ ["🗂", "card_index_dividers", 6],
+ ["🗞", "newspaper_roll", 6],
+ ["📰", "newspaper", 6],
+ ["📓", "notebook", 6],
+ ["📕", "closed_book", 6],
+ ["📗", "green_book", 6],
+ ["📘", "blue_book", 6],
+ ["📙", "orange_book", 6],
+ ["📔", "notebook_with_decorative_cover", 6],
+ ["📒", "ledger", 6],
+ ["📚", "books", 6],
+ ["📖", "open_book", 6],
+ ["🧷", "safety_pin", 6],
+ ["🔗", "link", 6],
+ ["📎", "paperclip", 6],
+ ["🖇", "paperclips", 6],
+ ["✂️", "scissors", 6],
+ ["📐", "triangular_ruler", 6],
+ ["📏", "straight_ruler", 6],
+ ["🧮", "abacus", 6],
+ ["📌", "pushpin", 6],
+ ["📍", "round_pushpin", 6],
+ ["🚩", "triangular_flag_on_post", 6],
+ ["🏳", "white_flag", 6],
+ ["🏴", "black_flag", 6],
+ ["🏳️‍🌈", "rainbow_flag", 6],
+ ["🏳️‍⚧️", "transgender_flag", 6],
+ ["🔐", "closed_lock_with_key", 6],
+ ["🔒", "lock", 6],
+ ["🔓", "unlock", 6],
+ ["🔏", "lock_with_ink_pen", 6],
+ ["🖊", "pen", 6],
+ ["🖋", "fountain_pen", 6],
+ ["✒️", "black_nib", 6],
+ ["📝", "memo", 6],
+ ["✏️", "pencil2", 6],
+ ["🖍", "crayon", 6],
+ ["🖌", "paintbrush", 6],
+ ["🔍", "mag", 6],
+ ["🔎", "mag_right", 6],
+ ["🪦", "headstone", 6],
+ ["🪧", "placard", 6],
+ ["💯", "100", 7],
+ ["🔢", "1234", 7],
+ ["🩷", "pink_heart", 7],
+ ["❤️", "heart", 7],
+ ["🧡", "orange_heart", 7],
+ ["💛", "yellow_heart", 7],
+ ["💚", "green_heart", 7],
+ ["🩵", "light_blue_heart", 7],
+ ["💙", "blue_heart", 7],
+ ["💜", "purple_heart", 7],
+ ["🤎", "brown_heart", 7],
+ ["🖤", "black_heart", 7],
+ ["🩶", "grey_heart", 7],
+ ["🤍", "white_heart", 7],
+ ["💔", "broken_heart", 7],
+ ["❣", "heavy_heart_exclamation", 7],
+ ["💕", "two_hearts", 7],
+ ["💞", "revolving_hearts", 7],
+ ["💓", "heartbeat", 7],
+ ["💗", "heartpulse", 7],
+ ["💖", "sparkling_heart", 7],
+ ["💘", "cupid", 7],
+ ["💝", "gift_heart", 7],
+ ["💟", "heart_decoration", 7],
+ ["❤️‍🔥", "heart_on_fire", 7],
+ ["❤️‍🩹", "mending_heart", 7],
+ ["☮", "peace_symbol", 7],
+ ["✝", "latin_cross", 7],
+ ["☪", "star_and_crescent", 7],
+ ["🕉", "om", 7],
+ ["☸", "wheel_of_dharma", 7],
+ ["🪯", "khanda", 7],
+ ["✡", "star_of_david", 7],
+ ["🔯", "six_pointed_star", 7],
+ ["🕎", "menorah", 7],
+ ["☯", "yin_yang", 7],
+ ["☦", "orthodox_cross", 7],
+ ["🛐", "place_of_worship", 7],
+ ["⛎", "ophiuchus", 7],
+ ["♈", "aries", 7],
+ ["♉", "taurus", 7],
+ ["♊", "gemini", 7],
+ ["♋", "cancer", 7],
+ ["♌", "leo", 7],
+ ["♍", "virgo", 7],
+ ["♎", "libra", 7],
+ ["♏", "scorpius", 7],
+ ["♐", "sagittarius", 7],
+ ["♑", "capricorn", 7],
+ ["♒", "aquarius", 7],
+ ["♓", "pisces", 7],
+ ["🆔", "id", 7],
+ ["⚛", "atom_symbol", 7],
+ ["⚧️", "transgender_symbol", 7],
+ ["🈳", "u7a7a", 7],
+ ["🈹", "u5272", 7],
+ ["☢", "radioactive", 7],
+ ["☣", "biohazard", 7],
+ ["📴", "mobile_phone_off", 7],
+ ["📳", "vibration_mode", 7],
+ ["🈶", "u6709", 7],
+ ["🈚", "u7121", 7],
+ ["🈸", "u7533", 7],
+ ["🈺", "u55b6", 7],
+ ["🈷️", "u6708", 7],
+ ["✴️", "eight_pointed_black_star", 7],
+ ["🆚", "vs", 7],
+ ["🉑", "accept", 7],
+ ["💮", "white_flower", 7],
+ ["🉐", "ideograph_advantage", 7],
+ ["㊙️", "secret", 7],
+ ["㊗️", "congratulations", 7],
+ ["🈴", "u5408", 7],
+ ["🈵", "u6e80", 7],
+ ["🈲", "u7981", 7],
+ ["🅰️", "a", 7],
+ ["🅱️", "b", 7],
+ ["🆎", "ab", 7],
+ ["🆑", "cl", 7],
+ ["🅾️", "o2", 7],
+ ["🆘", "sos", 7],
+ ["⛔", "no_entry", 7],
+ ["📛", "name_badge", 7],
+ ["🚫", "no_entry_sign", 7],
+ ["❌", "x", 7],
+ ["⭕", "o", 7],
+ ["🛑", "stop_sign", 7],
+ ["💢", "anger", 7],
+ ["♨️", "hotsprings", 7],
+ ["🚷", "no_pedestrians", 7],
+ ["🚯", "do_not_litter", 7],
+ ["🚳", "no_bicycles", 7],
+ ["🚱", "non-potable_water", 7],
+ ["🔞", "underage", 7],
+ ["📵", "no_mobile_phones", 7],
+ ["❗", "exclamation", 7],
+ ["❕", "grey_exclamation", 7],
+ ["❓", "question", 7],
+ ["❔", "grey_question", 7],
+ ["‼️", "bangbang", 7],
+ ["⁉️", "interrobang", 7],
+ ["🔅", "low_brightness", 7],
+ ["🔆", "high_brightness", 7],
+ ["🔱", "trident", 7],
+ ["⚜", "fleur_de_lis", 7],
+ ["〽️", "part_alternation_mark", 7],
+ ["⚠️", "warning", 7],
+ ["🚸", "children_crossing", 7],
+ ["🔰", "beginner", 7],
+ ["♻️", "recycle", 7],
+ ["🈯", "u6307", 7],
+ ["💹", "chart", 7],
+ ["❇️", "sparkle", 7],
+ ["✳️", "eight_spoked_asterisk", 7],
+ ["❎", "negative_squared_cross_mark", 7],
+ ["✅", "white_check_mark", 7],
+ ["💠", "diamond_shape_with_a_dot_inside", 7],
+ ["🌀", "cyclone", 7],
+ ["➿", "loop", 7],
+ ["🌐", "globe_with_meridians", 7],
+ ["Ⓜ️", "m", 7],
+ ["🏧", "atm", 7],
+ ["🈂️", "sa", 7],
+ ["🛂", "passport_control", 7],
+ ["🛃", "customs", 7],
+ ["🛄", "baggage_claim", 7],
+ ["🛅", "left_luggage", 7],
+ ["🛜", "wireless", 7],
+ ["♿", "wheelchair", 7],
+ ["🚭", "no_smoking", 7],
+ ["🚾", "wc", 7],
+ ["🅿️", "parking", 7],
+ ["🚰", "potable_water", 7],
+ ["🚹", "mens", 7],
+ ["🚺", "womens", 7],
+ ["🚼", "baby_symbol", 7],
+ ["🚻", "restroom", 7],
+ ["🚮", "put_litter_in_its_place", 7],
+ ["🎦", "cinema", 7],
+ ["📶", "signal_strength", 7],
+ ["🈁", "koko", 7],
+ ["🆖", "ng", 7],
+ ["🆗", "ok", 7],
+ ["🆙", "up", 7],
+ ["🆒", "cool", 7],
+ ["🆕", "new", 7],
+ ["🆓", "free", 7],
+ ["0️⃣", "zero", 7],
+ ["1️⃣", "one", 7],
+ ["2️⃣", "two", 7],
+ ["3️⃣", "three", 7],
+ ["4️⃣", "four", 7],
+ ["5️⃣", "five", 7],
+ ["6️⃣", "six", 7],
+ ["7️⃣", "seven", 7],
+ ["8️⃣", "eight", 7],
+ ["9️⃣", "nine", 7],
+ ["🔟", "keycap_ten", 7],
+ ["*⃣", "asterisk", 7],
+ ["⏏️", "eject_button", 7],
+ ["▶️", "arrow_forward", 7],
+ ["⏸", "pause_button", 7],
+ ["⏭", "next_track_button", 7],
+ ["⏹", "stop_button", 7],
+ ["⏺", "record_button", 7],
+ ["⏯", "play_or_pause_button", 7],
+ ["⏮", "previous_track_button", 7],
+ ["⏩", "fast_forward", 7],
+ ["⏪", "rewind", 7],
+ ["🔀", "twisted_rightwards_arrows", 7],
+ ["🔁", "repeat", 7],
+ ["🔂", "repeat_one", 7],
+ ["◀️", "arrow_backward", 7],
+ ["🔼", "arrow_up_small", 7],
+ ["🔽", "arrow_down_small", 7],
+ ["⏫", "arrow_double_up", 7],
+ ["⏬", "arrow_double_down", 7],
+ ["➡️", "arrow_right", 7],
+ ["⬅️", "arrow_left", 7],
+ ["⬆️", "arrow_up", 7],
+ ["⬇️", "arrow_down", 7],
+ ["↗️", "arrow_upper_right", 7],
+ ["↘️", "arrow_lower_right", 7],
+ ["↙️", "arrow_lower_left", 7],
+ ["↖️", "arrow_upper_left", 7],
+ ["↕️", "arrow_up_down", 7],
+ ["↔️", "left_right_arrow", 7],
+ ["🔄", "arrows_counterclockwise", 7],
+ ["↪️", "arrow_right_hook", 7],
+ ["↩️", "leftwards_arrow_with_hook", 7],
+ ["⤴️", "arrow_heading_up", 7],
+ ["⤵️", "arrow_heading_down", 7],
+ ["#️⃣", "hash", 7],
+ ["ℹ️", "information_source", 7],
+ ["🔤", "abc", 7],
+ ["🔡", "abcd", 7],
+ ["🔠", "capital_abcd", 7],
+ ["🔣", "symbols", 7],
+ ["🎵", "musical_note", 7],
+ ["🎶", "notes", 7],
+ ["〰️", "wavy_dash", 7],
+ ["➰", "curly_loop", 7],
+ ["✔️", "heavy_check_mark", 7],
+ ["🔃", "arrows_clockwise", 7],
+ ["➕", "heavy_plus_sign", 7],
+ ["➖", "heavy_minus_sign", 7],
+ ["➗", "heavy_division_sign", 7],
+ ["✖️", "heavy_multiplication_x", 7],
+ ["🟰", "heavy_equals_sign", 7],
+ ["♾", "infinity", 7],
+ ["💲", "heavy_dollar_sign", 7],
+ ["💱", "currency_exchange", 7],
+ ["©️", "copyright", 7],
+ ["®️", "registered", 7],
+ ["™️", "tm", 7],
+ ["🔚", "end", 7],
+ ["🔙", "back", 7],
+ ["🔛", "on", 7],
+ ["🔝", "top", 7],
+ ["🔜", "soon", 7],
+ ["☑️", "ballot_box_with_check", 7],
+ ["🔘", "radio_button", 7],
+ ["⚫", "black_circle", 7],
+ ["⚪", "white_circle", 7],
+ ["🔴", "red_circle", 7],
+ ["🟠", "orange_circle", 7],
+ ["🟡", "yellow_circle", 7],
+ ["🟢", "green_circle", 7],
+ ["🔵", "large_blue_circle", 7],
+ ["🟣", "purple_circle", 7],
+ ["🟤", "brown_circle", 7],
+ ["🔸", "small_orange_diamond", 7],
+ ["🔹", "small_blue_diamond", 7],
+ ["🔶", "large_orange_diamond", 7],
+ ["🔷", "large_blue_diamond", 7],
+ ["🔺", "small_red_triangle", 7],
+ ["▪️", "black_small_square", 7],
+ ["▫️", "white_small_square", 7],
+ ["⬛", "black_large_square", 7],
+ ["⬜", "white_large_square", 7],
+ ["🟥", "red_square", 7],
+ ["🟧", "orange_square", 7],
+ ["🟨", "yellow_square", 7],
+ ["🟩", "green_square", 7],
+ ["🟦", "blue_square", 7],
+ ["🟪", "purple_square", 7],
+ ["🟫", "brown_square", 7],
+ ["🔻", "small_red_triangle_down", 7],
+ ["◼️", "black_medium_square", 7],
+ ["◻️", "white_medium_square", 7],
+ ["◾", "black_medium_small_square", 7],
+ ["◽", "white_medium_small_square", 7],
+ ["🔲", "black_square_button", 7],
+ ["🔳", "white_square_button", 7],
+ ["🔈", "speaker", 7],
+ ["🔉", "sound", 7],
+ ["🔊", "loud_sound", 7],
+ ["🔇", "mute", 7],
+ ["📣", "mega", 7],
+ ["📢", "loudspeaker", 7],
+ ["🔔", "bell", 7],
+ ["🔕", "no_bell", 7],
+ ["🃏", "black_joker", 7],
+ ["🀄", "mahjong", 7],
+ ["♠️", "spades", 7],
+ ["♣️", "clubs", 7],
+ ["♥️", "hearts", 7],
+ ["♦️", "diamonds", 7],
+ ["🎴", "flower_playing_cards", 7],
+ ["💭", "thought_balloon", 7],
+ ["🗯", "right_anger_bubble", 7],
+ ["💬", "speech_balloon", 7],
+ ["🗨", "left_speech_bubble", 7],
+ ["🕐", "clock1", 7],
+ ["🕑", "clock2", 7],
+ ["🕒", "clock3", 7],
+ ["🕓", "clock4", 7],
+ ["🕔", "clock5", 7],
+ ["🕕", "clock6", 7],
+ ["🕖", "clock7", 7],
+ ["🕗", "clock8", 7],
+ ["🕘", "clock9", 7],
+ ["🕙", "clock10", 7],
+ ["🕚", "clock11", 7],
+ ["🕛", "clock12", 7],
+ ["🕜", "clock130", 7],
+ ["🕝", "clock230", 7],
+ ["🕞", "clock330", 7],
+ ["🕟", "clock430", 7],
+ ["🕠", "clock530", 7],
+ ["🕡", "clock630", 7],
+ ["🕢", "clock730", 7],
+ ["🕣", "clock830", 7],
+ ["🕤", "clock930", 7],
+ ["🕥", "clock1030", 7],
+ ["🕦", "clock1130", 7],
+ ["🕧", "clock1230", 7],
+ ["🇦🇫", "afghanistan", 8],
+ ["🇦🇽", "aland_islands", 8],
+ ["🇦🇱", "albania", 8],
+ ["🇩🇿", "algeria", 8],
+ ["🇦🇸", "american_samoa", 8],
+ ["🇦🇩", "andorra", 8],
+ ["🇦🇴", "angola", 8],
+ ["🇦🇮", "anguilla", 8],
+ ["🇦🇶", "antarctica", 8],
+ ["🇦🇬", "antigua_barbuda", 8],
+ ["🇦🇷", "argentina", 8],
+ ["🇦🇲", "armenia", 8],
+ ["🇦🇼", "aruba", 8],
+ ["🇦🇨", "ascension_island", 8],
+ ["🇦🇺", "australia", 8],
+ ["🇦🇹", "austria", 8],
+ ["🇦🇿", "azerbaijan", 8],
+ ["🇧🇸", "bahamas", 8],
+ ["🇧🇭", "bahrain", 8],
+ ["🇧🇩", "bangladesh", 8],
+ ["🇧🇧", "barbados", 8],
+ ["🇧🇾", "belarus", 8],
+ ["🇧🇪", "belgium", 8],
+ ["🇧🇿", "belize", 8],
+ ["🇧🇯", "benin", 8],
+ ["🇧🇲", "bermuda", 8],
+ ["🇧🇹", "bhutan", 8],
+ ["🇧🇴", "bolivia", 8],
+ ["🇧🇶", "caribbean_netherlands", 8],
+ ["🇧🇦", "bosnia_herzegovina", 8],
+ ["🇧🇼", "botswana", 8],
+ ["🇧🇷", "brazil", 8],
+ ["🇮🇴", "british_indian_ocean_territory", 8],
+ ["🇻🇬", "british_virgin_islands", 8],
+ ["🇧🇳", "brunei", 8],
+ ["🇧🇬", "bulgaria", 8],
+ ["🇧🇫", "burkina_faso", 8],
+ ["🇧🇮", "burundi", 8],
+ ["🇨🇻", "cape_verde", 8],
+ ["🇰🇭", "cambodia", 8],
+ ["🇨🇲", "cameroon", 8],
+ ["🇨🇦", "canada", 8],
+ ["🇮🇨", "canary_islands", 8],
+ ["🇰🇾", "cayman_islands", 8],
+ ["🇨🇫", "central_african_republic", 8],
+ ["🇹🇩", "chad", 8],
+ ["🇨🇱", "chile", 8],
+ ["🇨🇳", "cn", 8],
+ ["🇨🇽", "christmas_island", 8],
+ ["🇨🇨", "cocos_islands", 8],
+ ["🇨🇴", "colombia", 8],
+ ["🇰🇲", "comoros", 8],
+ ["🇨🇬", "congo_brazzaville", 8],
+ ["🇨🇩", "congo_kinshasa", 8],
+ ["🇨🇰", "cook_islands", 8],
+ ["🇨🇷", "costa_rica", 8],
+ ["🇭🇷", "croatia", 8],
+ ["🇨🇺", "cuba", 8],
+ ["🇨🇼", "curacao", 8],
+ ["🇨🇾", "cyprus", 8],
+ ["🇨🇿", "czech_republic", 8],
+ ["🇩🇰", "denmark", 8],
+ ["🇩🇯", "djibouti", 8],
+ ["🇩🇲", "dominica", 8],
+ ["🇩🇴", "dominican_republic", 8],
+ ["🇪🇨", "ecuador", 8],
+ ["🇪🇬", "egypt", 8],
+ ["🇸🇻", "el_salvador", 8],
+ ["🇬🇶", "equatorial_guinea", 8],
+ ["🇪🇷", "eritrea", 8],
+ ["🇪🇪", "estonia", 8],
+ ["🇪🇹", "ethiopia", 8],
+ ["🇪🇺", "eu", 8],
+ ["🇫🇰", "falkland_islands", 8],
+ ["🇫🇴", "faroe_islands", 8],
+ ["🇫🇯", "fiji", 8],
+ ["🇫🇮", "finland", 8],
+ ["🇫🇷", "fr", 8],
+ ["🇬🇫", "french_guiana", 8],
+ ["🇵🇫", "french_polynesia", 8],
+ ["🇹🇫", "french_southern_territories", 8],
+ ["🇬🇦", "gabon", 8],
+ ["🇬🇲", "gambia", 8],
+ ["🇬🇪", "georgia", 8],
+ ["🇩🇪", "de", 8],
+ ["🇬🇭", "ghana", 8],
+ ["🇬🇮", "gibraltar", 8],
+ ["🇬🇷", "greece", 8],
+ ["🇬🇱", "greenland", 8],
+ ["🇬🇩", "grenada", 8],
+ ["🇬🇵", "guadeloupe", 8],
+ ["🇬🇺", "guam", 8],
+ ["🇬🇹", "guatemala", 8],
+ ["🇬🇬", "guernsey", 8],
+ ["🇬🇳", "guinea", 8],
+ ["🇬🇼", "guinea_bissau", 8],
+ ["🇬🇾", "guyana", 8],
+ ["🇭🇹", "haiti", 8],
+ ["🇭🇳", "honduras", 8],
+ ["🇭🇰", "hong_kong", 8],
+ ["🇭🇺", "hungary", 8],
+ ["🇮🇸", "iceland", 8],
+ ["🇮🇳", "india", 8],
+ ["🇮🇩", "indonesia", 8],
+ ["🇮🇷", "iran", 8],
+ ["🇮🇶", "iraq", 8],
+ ["🇮🇪", "ireland", 8],
+ ["🇮🇲", "isle_of_man", 8],
+ ["🇮🇱", "israel", 8],
+ ["🇮🇹", "it", 8],
+ ["🇨🇮", "cote_divoire", 8],
+ ["🇯🇲", "jamaica", 8],
+ ["🇯🇵", "jp", 8],
+ ["🇯🇪", "jersey", 8],
+ ["🇯🇴", "jordan", 8],
+ ["🇰🇿", "kazakhstan", 8],
+ ["🇰🇪", "kenya", 8],
+ ["🇰🇮", "kiribati", 8],
+ ["🇽🇰", "kosovo", 8],
+ ["🇰🇼", "kuwait", 8],
+ ["🇰🇬", "kyrgyzstan", 8],
+ ["🇱🇦", "laos", 8],
+ ["🇱🇻", "latvia", 8],
+ ["🇱🇧", "lebanon", 8],
+ ["🇱🇸", "lesotho", 8],
+ ["🇱🇷", "liberia", 8],
+ ["🇱🇾", "libya", 8],
+ ["🇱🇮", "liechtenstein", 8],
+ ["🇱🇹", "lithuania", 8],
+ ["🇱🇺", "luxembourg", 8],
+ ["🇲🇴", "macau", 8],
+ ["🇲🇰", "macedonia", 8],
+ ["🇲🇬", "madagascar", 8],
+ ["🇲🇼", "malawi", 8],
+ ["🇲🇾", "malaysia", 8],
+ ["🇲🇻", "maldives", 8],
+ ["🇲🇱", "mali", 8],
+ ["🇲🇹", "malta", 8],
+ ["🇲🇭", "marshall_islands", 8],
+ ["🇲🇶", "martinique", 8],
+ ["🇲🇷", "mauritania", 8],
+ ["🇲🇺", "mauritius", 8],
+ ["🇾🇹", "mayotte", 8],
+ ["🇲🇽", "mexico", 8],
+ ["🇫🇲", "micronesia", 8],
+ ["🇲🇩", "moldova", 8],
+ ["🇲🇨", "monaco", 8],
+ ["🇲🇳", "mongolia", 8],
+ ["🇲🇪", "montenegro", 8],
+ ["🇲🇸", "montserrat", 8],
+ ["🇲🇦", "morocco", 8],
+ ["🇲🇿", "mozambique", 8],
+ ["🇲🇲", "myanmar", 8],
+ ["🇳🇦", "namibia", 8],
+ ["🇳🇷", "nauru", 8],
+ ["🇳🇵", "nepal", 8],
+ ["🇳🇱", "netherlands", 8],
+ ["🇳🇨", "new_caledonia", 8],
+ ["🇳🇿", "new_zealand", 8],
+ ["🇳🇮", "nicaragua", 8],
+ ["🇳🇪", "niger", 8],
+ ["🇳🇬", "nigeria", 8],
+ ["🇳🇺", "niue", 8],
+ ["🇳🇫", "norfolk_island", 8],
+ ["🇲🇵", "northern_mariana_islands", 8],
+ ["🇰🇵", "north_korea", 8],
+ ["🇳🇴", "norway", 8],
+ ["🇴🇲", "oman", 8],
+ ["🇵🇰", "pakistan", 8],
+ ["🇵🇼", "palau", 8],
+ ["🇵🇸", "palestinian_territories", 8],
+ ["🇵🇦", "panama", 8],
+ ["🇵🇬", "papua_new_guinea", 8],
+ ["🇵🇾", "paraguay", 8],
+ ["🇵🇪", "peru", 8],
+ ["🇵🇭", "philippines", 8],
+ ["🇵🇳", "pitcairn_islands", 8],
+ ["🇵🇱", "poland", 8],
+ ["🇵🇹", "portugal", 8],
+ ["🇵🇷", "puerto_rico", 8],
+ ["🇶🇦", "qatar", 8],
+ ["🇷🇪", "reunion", 8],
+ ["🇷🇴", "romania", 8],
+ ["🇷🇺", "ru", 8],
+ ["🇷🇼", "rwanda", 8],
+ ["🇧🇱", "st_barthelemy", 8],
+ ["🇸🇭", "st_helena", 8],
+ ["🇰🇳", "st_kitts_nevis", 8],
+ ["🇱🇨", "st_lucia", 8],
+ ["🇵🇲", "st_pierre_miquelon", 8],
+ ["🇻🇨", "st_vincent_grenadines", 8],
+ ["🇼🇸", "samoa", 8],
+ ["🇸🇲", "san_marino", 8],
+ ["🇸🇹", "sao_tome_principe", 8],
+ ["🇸🇦", "saudi_arabia", 8],
+ ["🇸🇳", "senegal", 8],
+ ["🇷🇸", "serbia", 8],
+ ["🇸🇨", "seychelles", 8],
+ ["🇸🇱", "sierra_leone", 8],
+ ["🇸🇬", "singapore", 8],
+ ["🇸🇽", "sint_maarten", 8],
+ ["🇸🇰", "slovakia", 8],
+ ["🇸🇮", "slovenia", 8],
+ ["🇸🇧", "solomon_islands", 8],
+ ["🇸🇴", "somalia", 8],
+ ["🇿🇦", "south_africa", 8],
+ ["🇬🇸", "south_georgia_south_sandwich_islands", 8],
+ ["🇰🇷", "kr", 8],
+ ["🇸🇸", "south_sudan", 8],
+ ["🇪🇸", "es", 8],
+ ["🇱🇰", "sri_lanka", 8],
+ ["🇸🇩", "sudan", 8],
+ ["🇸🇷", "suriname", 8],
+ ["🇸🇿", "swaziland", 8],
+ ["🇸🇪", "sweden", 8],
+ ["🇨🇭", "switzerland", 8],
+ ["🇸🇾", "syria", 8],
+ ["🇹🇼", "taiwan", 8],
+ ["🇹🇯", "tajikistan", 8],
+ ["🇹🇿", "tanzania", 8],
+ ["🇹🇭", "thailand", 8],
+ ["🇹🇱", "timor_leste", 8],
+ ["🇹🇬", "togo", 8],
+ ["🇹🇰", "tokelau", 8],
+ ["🇹🇴", "tonga", 8],
+ ["🇹🇹", "trinidad_tobago", 8],
+ ["🇹🇦", "tristan_da_cunha", 8],
+ ["🇹🇳", "tunisia", 8],
+ ["🇹🇷", "tr", 8],
+ ["🇹🇲", "turkmenistan", 8],
+ ["🇹🇨", "turks_caicos_islands", 8],
+ ["🇹🇻", "tuvalu", 8],
+ ["🇺🇬", "uganda", 8],
+ ["🇺🇦", "ukraine", 8],
+ ["🇦🇪", "united_arab_emirates", 8],
+ ["🇬🇧", "uk", 8],
+ ["🏴󠁧󠁢󠁥󠁮󠁧󠁿", "england", 8],
+ ["🏴󠁧󠁢󠁳󠁣󠁴󠁿", "scotland", 8],
+ ["🏴󠁧󠁢󠁷󠁬󠁳󠁿", "wales", 8],
+ ["🇺🇸", "us", 8],
+ ["🇻🇮", "us_virgin_islands", 8],
+ ["🇺🇾", "uruguay", 8],
+ ["🇺🇿", "uzbekistan", 8],
+ ["🇻🇺", "vanuatu", 8],
+ ["🇻🇦", "vatican_city", 8],
+ ["🇻🇪", "venezuela", 8],
+ ["🇻🇳", "vietnam", 8],
+ ["🇼🇫", "wallis_futuna", 8],
+ ["🇪🇭", "western_sahara", 8],
+ ["🇾🇪", "yemen", 8],
+ ["🇿🇲", "zambia", 8],
+ ["🇿🇼", "zimbabwe", 8],
+ ["🇺🇳", "united_nations", 8],
+ ["🏴‍☠️", "pirate_flag", 8]
+]
diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts
new file mode 100644
index 0000000000..bde30a864f
--- /dev/null
+++ b/packages/frontend-shared/js/emojilist.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
+
+export type UnicodeEmojiDef = {
+ name: string;
+ char: string;
+ category: typeof unicodeEmojiCategories[number];
+}
+
+// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
+import _emojilist from './emojilist.json';
+
+export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
+ name: x[1] as string,
+ char: x[0] as string,
+ category: unicodeEmojiCategories[x[2] as number],
+}));
+
+const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>(
+ emojilist.map(x => [x.char, x]),
+);
+
+const _indexByChar = new Map<string, number>();
+const _charGroupByCategory = new Map<string, string[]>();
+for (let i = 0; i < emojilist.length; i++) {
+ const emo = emojilist[i];
+ _indexByChar.set(emo.char, i);
+
+ if (_charGroupByCategory.has(emo.category)) {
+ _charGroupByCategory.get(emo.category)?.push(emo.char);
+ } else {
+ _charGroupByCategory.set(emo.category, [emo.char]);
+ }
+}
+
+export const emojiCharByCategory = _charGroupByCategory;
+
+export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
+ // Colorize it because emojilist.json assumes that
+ return unicodeEmojisMap.get(colorizeEmoji(char))
+ // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
+ ?? unicodeEmojisMap.get(char)
+ // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する)
+ ?? char;
+}
+
+export function getEmojiName(char: string): string {
+ // Colorize it because emojilist.json assumes that
+ const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
+ if (idx === undefined) {
+ // 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い
+ return char;
+ } else {
+ return emojilist[idx].name;
+ }
+}
+
+/**
+ * テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。
+ */
+export function colorizeEmoji(char: string) {
+ return char.length === 1 ? `${char}\uFE0F` : char;
+}
+
+export interface CustomEmojiFolderTree {
+ value: string;
+ category: string;
+ children: CustomEmojiFolderTree[];
+}
diff --git a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
new file mode 100644
index 0000000000..992f6e9a16
--- /dev/null
+++ b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function extractAvgColorFromBlurhash(hash: string) {
+ return typeof hash === 'string'
+ ? '#' + [...hash.slice(2, 6)]
+ .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
+ .reduce((a, c) => a * 83 + c, 0)
+ .toString(16)
+ .padStart(6, '0')
+ : undefined;
+}
diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend-shared/js/i18n.ts
new file mode 100644
index 0000000000..18232691fa
--- /dev/null
+++ b/packages/frontend-shared/js/i18n.ts
@@ -0,0 +1,251 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import type { ILocale, ParameterizedString } from '../../../locales/index.js';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type TODO = any;
+
+type FlattenKeys<T extends ILocale, TPrediction> = keyof {
+ [K in keyof T as T[K] extends ILocale
+ ? FlattenKeys<T[K], TPrediction> extends infer C extends string
+ ? `${K & string}.${C}`
+ : never
+ : T[K] extends TPrediction
+ ? K
+ : never]: T[K];
+};
+
+type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}`
+ // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
+ ? ParametersOf<T[K], C>
+ : TKey extends keyof T
+ ? T[TKey] extends ParameterizedString<infer P>
+ ? P
+ : never
+ : never;
+
+type Tsx<T extends ILocale> = {
+ readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
+ ? (arg: { readonly [_ in P]: string | number }) => string
+ // @ts-expect-error -- 証明省略
+ : Tsx<T[K]>;
+};
+
+export class I18n<T extends ILocale> {
+ private tsxCache?: Tsx<T>;
+ private devMode: boolean;
+
+ constructor(public locale: T, devMode = false) {
+ this.devMode = devMode;
+
+ //#region BIND
+ this.t = this.t.bind(this);
+ //#endregion
+ }
+
+ public get ts(): T {
+ if (this.devMode) {
+ class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
+ get(target: TTarget, p: string | symbol): unknown {
+ const value = target[p as keyof TTarget];
+
+ if (typeof value === 'object') {
+ return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
+ }
+
+ if (typeof value === 'string') {
+ const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter);
+
+ if (parameters.length) {
+ console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
+ }
+
+ return value;
+ }
+
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return p;
+ }
+ }
+
+ return new Proxy(this.locale, new Handler());
+ }
+
+ return this.locale;
+ }
+
+ public get tsx(): Tsx<T> {
+ if (this.devMode) {
+ if (this.tsxCache) {
+ return this.tsxCache;
+ }
+
+ class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
+ get(target: TTarget, p: string | symbol): unknown {
+ const value = target[p as keyof TTarget];
+
+ if (typeof value === 'object') {
+ return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
+ }
+
+ if (typeof value === 'string') {
+ const quasis: string[] = [];
+ const expressions: string[] = [];
+ let cursor = 0;
+
+ while (~cursor) {
+ const start = value.indexOf('{', cursor);
+
+ if (!~start) {
+ quasis.push(value.slice(cursor));
+ break;
+ }
+
+ quasis.push(value.slice(cursor, start));
+
+ const end = value.indexOf('}', start);
+
+ expressions.push(value.slice(start + 1, end));
+
+ cursor = end + 1;
+ }
+
+ if (!expressions.length) {
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return () => value;
+ }
+
+ return (arg: TODO) => {
+ let str = quasis[0];
+
+ for (let i = 0; i < expressions.length; i++) {
+ if (!Object.hasOwn(arg, expressions[i])) {
+ console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`);
+ }
+
+ str += arg[expressions[i]] + quasis[i + 1];
+ }
+
+ return str;
+ };
+ }
+
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return p;
+ }
+ }
+
+ return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
+ }
+
+ if (this.tsxCache) {
+ return this.tsxCache;
+ }
+
+ function build(target: ILocale): Tsx<T> {
+ const result = {} as Tsx<T>;
+
+ for (const k in target) {
+ if (!Object.hasOwn(target, k)) {
+ continue;
+ }
+
+ const value = target[k as keyof typeof target];
+
+ if (typeof value === 'object') {
+ (result as TODO)[k] = build(value as ILocale);
+ } else if (typeof value === 'string') {
+ const quasis: string[] = [];
+ const expressions: string[] = [];
+ let cursor = 0;
+
+ while (~cursor) {
+ const start = value.indexOf('{', cursor);
+
+ if (!~start) {
+ quasis.push(value.slice(cursor));
+ break;
+ }
+
+ quasis.push(value.slice(cursor, start));
+
+ const end = value.indexOf('}', start);
+
+ expressions.push(value.slice(start + 1, end));
+
+ cursor = end + 1;
+ }
+
+ if (!expressions.length) {
+ continue;
+ }
+
+ (result as TODO)[k] = (arg: TODO) => {
+ let str = quasis[0];
+
+ for (let i = 0; i < expressions.length; i++) {
+ str += arg[expressions[i]] + quasis[i + 1];
+ }
+
+ return str;
+ };
+ }
+ }
+ return result;
+ }
+
+ return this.tsxCache = build(this.locale);
+ }
+
+ /**
+ * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
+ */
+ public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
+ /**
+ * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
+ */
+ public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
+ public t(key: string, args?: { readonly [_: string]: string | number }) {
+ let str: string | ParameterizedString | ILocale = this.locale;
+
+ for (const k of key.split('.')) {
+ str = (str as TODO)[k];
+
+ if (this.devMode) {
+ if (typeof str === 'undefined') {
+ console.error(`Unexpected locale key: ${key}`);
+ return key;
+ }
+ }
+ }
+
+ if (args) {
+ if (this.devMode) {
+ const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
+
+ if (missing.length) {
+ console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
+ }
+ }
+
+ for (const [k, v] of Object.entries(args)) {
+ const search = `{${k}}`;
+
+ if (this.devMode) {
+ if (!(str as string).includes(search)) {
+ console.error(`Unexpected locale parameter: ${k} at ${key}`);
+ }
+ }
+
+ str = (str as string).replace(search, v.toString());
+ }
+ }
+
+ return str;
+ }
+}
diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts
new file mode 100644
index 0000000000..2837870c9a
--- /dev/null
+++ b/packages/frontend-shared/js/media-proxy.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { query } from './url.js';
+
+export class MediaProxy {
+ private serverMetadata: Misskey.entities.MetaDetailed;
+ private url: string;
+
+ constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) {
+ this.serverMetadata = serverMetadata;
+ this.url = url;
+ }
+
+ public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
+ const localProxy = `${this.url}/proxy`;
+ let _imageUrl = imageUrl;
+
+ if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
+ // もう既にproxyっぽそうだったらurlを取り出す
+ _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+ }
+
+ return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${
+ type === 'preview' ? 'preview.webp'
+ : 'image.webp'
+ }?${query({
+ url: _imageUrl,
+ ...(!noFallback ? { 'fallback': '1' } : {}),
+ ...(type ? { [type]: '1' } : {}),
+ ...(mustOrigin ? { origin: '1' } : {}),
+ })}`;
+ }
+
+ public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
+ if (imageUrl == null) return null;
+ return this.getProxiedImageUrl(imageUrl, type);
+ }
+
+ public getStaticImageUrl(baseUrl: string): string {
+ const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url);
+
+ if (u.href.startsWith(`${this.url}/emoji/`)) {
+ // もう既にemojiっぽそうだったらsearchParams付けるだけ
+ u.searchParams.set('static', '1');
+ return u.href;
+ }
+
+ if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) {
+ // もう既にproxyっぽそうだったらsearchParams付けるだけ
+ u.searchParams.set('static', '1');
+ return u.href;
+ }
+
+ return `${this.serverMetadata.mediaProxy}/static.webp?${query({
+ url: u.href,
+ static: '1',
+ })}`;
+ }
+}
diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts
new file mode 100644
index 0000000000..1062e5252f
--- /dev/null
+++ b/packages/frontend-shared/js/scroll.ts
@@ -0,0 +1,144 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type ScrollBehavior = 'auto' | 'smooth' | 'instant';
+
+export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
+ if (el == null || el.tagName === 'HTML') return null;
+ const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y');
+ if (overflow === 'scroll' || overflow === 'auto') {
+ return el;
+ } else {
+ return getScrollContainer(el.parentElement);
+ }
+}
+
+export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) {
+ if (!el.parentElement) return top;
+ const data = el.dataset.stickyContainerHeaderHeight;
+ const newTop = data ? Number(data) + top : top;
+ if (el === container) return newTop;
+ return getStickyTop(el.parentElement, container, newTop);
+}
+
+export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) {
+ if (!el.parentElement) return bottom;
+ const data = el.dataset.stickyContainerFooterHeight;
+ const newBottom = data ? Number(data) + bottom : bottom;
+ if (el === container) return newBottom;
+ return getStickyBottom(el.parentElement, container, newBottom);
+}
+
+export function getScrollPosition(el: HTMLElement | null): number {
+ const container = getScrollContainer(el);
+ return container == null ? window.scrollY : container.scrollTop;
+}
+
+export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
+ // とりあえず評価してみる
+ if (el.isConnected && isTopVisible(el)) {
+ cb();
+ if (once) return null;
+ }
+
+ const container = getScrollContainer(el) ?? window;
+
+ const onScroll = () => {
+ if (!document.body.contains(el)) return;
+ if (isTopVisible(el, tolerance)) {
+ cb();
+ if (once) removeListener();
+ }
+ };
+
+ function removeListener() { container.removeEventListener('scroll', onScroll); }
+
+ container.addEventListener('scroll', onScroll, { passive: true });
+ return removeListener;
+}
+
+export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
+ const container = getScrollContainer(el);
+
+ // とりあえず評価してみる
+ if (el.isConnected && isBottomVisible(el, tolerance, container)) {
+ cb();
+ if (once) return null;
+ }
+
+ const containerOrWindow = container ?? window;
+ const onScroll = () => {
+ if (!document.body.contains(el)) return;
+ if (isBottomVisible(el, 1, container)) {
+ cb();
+ if (once) removeListener();
+ }
+ };
+
+ function removeListener() {
+ containerOrWindow.removeEventListener('scroll', onScroll);
+ }
+
+ containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
+ return removeListener;
+}
+
+export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
+ const container = getScrollContainer(el);
+ if (container == null) {
+ window.scroll(options);
+ } else {
+ container.scroll(options);
+ }
+}
+
+/**
+ * Scroll to Top
+ * @param el Scroll container element
+ * @param options Scroll options
+ */
+export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
+ scroll(el, { top: 0, ...options });
+}
+
+/**
+ * Scroll to Bottom
+ * @param el Content element
+ * @param options Scroll options
+ * @param container Scroll container element
+ */
+export function scrollToBottom(
+ el: HTMLElement,
+ options: ScrollToOptions = {},
+ container = getScrollContainer(el),
+) {
+ if (container) {
+ container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options });
+ } else {
+ window.scroll({
+ top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
+ ...options,
+ });
+ }
+}
+
+export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
+ const scrollTop = getScrollPosition(el);
+ return scrollTop <= tolerance;
+}
+
+export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
+ if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
+ return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
+}
+
+// https://ja.javascript.info/size-and-scroll-window#ref-932
+export function getBodyScrollHeight() {
+ return Math.max(
+ document.body.scrollHeight, document.documentElement.scrollHeight,
+ document.body.offsetHeight, document.documentElement.offsetHeight,
+ document.body.clientHeight, document.documentElement.clientHeight,
+ );
+}
diff --git a/packages/frontend-shared/js/url.ts b/packages/frontend-shared/js/url.ts
new file mode 100644
index 0000000000..eb830b1eea
--- /dev/null
+++ b/packages/frontend-shared/js/url.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* objを検査して
+ * 1. 配列に何も入っていない時はクエリを付けない
+ * 2. プロパティがundefinedの時はクエリを付けない
+ * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
+ */
+export function query(obj: Record<string, string | number | boolean>): string {
+ const params = Object.entries(obj)
+ .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition
+ .reduce<Record<string, string | number | boolean>>((a, [k, v]) => (a[k] = v, a), {});
+
+ return Object.entries(params)
+ .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
+ .join('&');
+}
+
+export function appendQuery(url: string, queryString: string): string {
+ return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`;
+}
+
+export function extractDomain(url: string) {
+ const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
+ return match ? match[1] : null;
+}
diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts
new file mode 100644
index 0000000000..b1197e68da
--- /dev/null
+++ b/packages/frontend-shared/js/use-document-visibility.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { onMounted, onUnmounted, ref } from 'vue';
+import type { Ref } from 'vue';
+
+export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
+ const visibility = ref(document.visibilityState);
+
+ const onChange = (): void => {
+ visibility.value = document.visibilityState;
+ };
+
+ onMounted(() => {
+ document.addEventListener('visibilitychange', onChange);
+ });
+
+ onUnmounted(() => {
+ document.removeEventListener('visibilitychange', onChange);
+ });
+
+ return visibility;
+}
diff --git a/packages/frontend-shared/js/use-interval.ts b/packages/frontend-shared/js/use-interval.ts
new file mode 100644
index 0000000000..b50e78c3cc
--- /dev/null
+++ b/packages/frontend-shared/js/use-interval.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
+
+export function useInterval(fn: () => void, interval: number, options: {
+ immediate: boolean;
+ afterMounted: boolean;
+}): (() => void) | undefined {
+ if (Number.isNaN(interval)) return;
+
+ let intervalId: number | null = null;
+
+ if (options.afterMounted) {
+ onMounted(() => {
+ if (options.immediate) fn();
+ intervalId = window.setInterval(fn, interval);
+ });
+ } else {
+ if (options.immediate) fn();
+ intervalId = window.setInterval(fn, interval);
+ }
+
+ const clear = () => {
+ if (intervalId) window.clearInterval(intervalId);
+ intervalId = null;
+ };
+
+ onActivated(() => {
+ if (intervalId) return;
+ if (options.immediate) fn();
+ intervalId = window.setInterval(fn, interval);
+ });
+
+ onDeactivated(() => {
+ clear();
+ });
+
+ onUnmounted(() => {
+ clear();
+ });
+
+ return clear;
+}