| 1 | export interface EmojiEntry { |
| 2 | emoji: string; |
| 3 | label: string; |
| 4 | tags: string[]; |
| 5 | shortcodes: string[]; |
| 6 | } |
| 7 | |
| 8 | export interface EmojiGroup { |
| 9 | name: string; |
| 10 | emojis: EmojiEntry[]; |
| 11 | } |
| 12 | |
| 13 | interface RawEmoji { |
| 14 | hexcode: string; |
| 15 | unicode: string; |
| 16 | label: string; |
| 17 | group?: number; |
| 18 | order?: number; |
| 19 | tags?: string[]; |
| 20 | } |
| 21 | |
| 22 | const GROUP_NAMES: Record<number, string> = { |
| 23 | 0: 'Smileys & emotion', |
| 24 | 1: 'People & body', |
| 25 | 3: 'Animals & nature', |
| 26 | 4: 'Food & drink', |
| 27 | 5: 'Travel & places', |
| 28 | 6: 'Activities', |
| 29 | 7: 'Objects', |
| 30 | 8: 'Symbols', |
| 31 | 9: 'Flags' |
| 32 | }; |
| 33 | |
| 34 | let loading: Promise<EmojiGroup[]> | null = null; |
| 35 | |
| 36 | export function loadEmoji(): Promise<EmojiGroup[]> { |
| 37 | loading ??= (async () => { |
| 38 | const [compact, codes] = await Promise.all([ |
| 39 | import('emojibase-data/en/compact.json'), |
| 40 | import('emojibase-data/en/shortcodes/emojibase.json') |
| 41 | ]); |
| 42 | const shortcodes = codes.default as Record<string, string | string[]>; |
| 43 | const byGroup = new Map<number, EmojiEntry[]>(); |
| 44 | for (const raw of compact.default as RawEmoji[]) { |
| 45 | if (raw.group === undefined || !(raw.group in GROUP_NAMES)) continue; |
| 46 | const code = shortcodes[raw.hexcode]; |
| 47 | const entry: EmojiEntry = { |
| 48 | emoji: raw.unicode, |
| 49 | label: raw.label, |
| 50 | tags: raw.tags ?? [], |
| 51 | shortcodes: code === undefined ? [] : Array.isArray(code) ? code : [code] |
| 52 | }; |
| 53 | const list = byGroup.get(raw.group) ?? []; |
| 54 | list.push(entry); |
| 55 | byGroup.set(raw.group, list); |
| 56 | } |
| 57 | return Object.entries(GROUP_NAMES).flatMap(([id, name]) => { |
| 58 | const emojis = byGroup.get(Number(id)); |
| 59 | return emojis ? [{ name, emojis }] : []; |
| 60 | }); |
| 61 | })().catch((e) => { |
| 62 | loading = null; |
| 63 | throw e; |
| 64 | }); |
| 65 | return loading; |
| 66 | } |
| 67 | |
| 68 | export function matchesEmoji(entry: EmojiEntry, query: string): boolean { |
| 69 | const q = query.trim().toLowerCase().replace(/^:|:$/g, ''); |
| 70 | if (!q) return true; |
| 71 | return ( |
| 72 | entry.label.toLowerCase().includes(q) || |
| 73 | entry.tags.some((t) => t.toLowerCase().includes(q)) || |
| 74 | entry.shortcodes.some((s) => s.toLowerCase().includes(q)) |
| 75 | ); |
| 76 | } |
| 77 | |
| 78 | export function searchEmoji(groups: EmojiGroup[], query: string): EmojiGroup[] { |
| 79 | return groups |
| 80 | .map((g) => ({ name: g.name, emojis: g.emojis.filter((e) => matchesEmoji(e, query)) })) |
| 81 | .filter((g) => g.emojis.length > 0); |
| 82 | } |