emoji.ts
1 import type { MastodonAccount } from '$lib/mastodon'; 2 import emojiRegex from 'emoji-regex'; 3 4 interface EmojiEntry { 5 id: string; 6 char?: string; 7 url?: string; 8 count: number; 9 label: string; 10 } 11 12 const EMOJI_REGEX = emojiRegex(); 13 const CUSTOM_EMOJI_REGEX = /:([a-zA-Z0-9_]{2,30}):/g; 14 15 /** 16 * Robust emoji extraction using emoji-regex for Unicode 17 * and custom regex for Mastodon shortcodes. 18 */ 19 /** 20 * Extracts a unique set of emoji keys from a single account's data. 21 */ 22 function getAccountEmojiKeys(account: MastodonAccount): Set<string> { 23 const texts = [ 24 account.display_name, 25 account.note, 26 ...(account.fields || []).map((f) => `${f.name} ${f.value}`) 27 ]; 28 29 return texts.reduce((keys, text) => { 30 if (!text) return keys; 31 32 // 1. Unicode Emojis using industry standard regex 33 for (const match of text.matchAll(EMOJI_REGEX)) { 34 const char = match[0]; 35 if (char.length === 1 && char >= '0' && char <= '9') continue; 36 keys.add(char); 37 } 38 39 // 2. Custom Emojis (Shortcodes) 40 for (const match of text.matchAll(CUSTOM_EMOJI_REGEX)) { 41 const shortcode = match[1]; 42 const config = account.emojis?.find((e) => e.shortcode === shortcode); 43 if (config) { 44 keys.add(`custom:${shortcode}|${config.static_url || config.url}`); 45 } 46 } 47 48 return keys; 49 }, new Set<string>()); 50 } 51 52 /** 53 * Updates an aggregation map with a single emoji key. 54 */ 55 function upsertEmojiEntry(counts: Map<string, EmojiEntry>, key: string): void { 56 if (key.startsWith('custom:')) { 57 const [id, url] = key.split('|'); 58 const shortcode = id.split(':')[1]; 59 const entry = counts.get(id) || { 60 id, 61 url, 62 count: 0, 63 label: `:${shortcode}:` 64 }; 65 entry.count++; 66 counts.set(id, entry); 67 } else { 68 const entry = counts.get(key) || { id: key, char: key, count: 0, label: key }; 69 entry.count++; 70 counts.set(key, entry); 71 } 72 } 73 74 /** 75 * Robust emoji extraction from a list of accounts. 76 * Returns a Map of emoji keys to EmojiEntry objects. 77 */ 78 export function extractEmojis(accounts: MastodonAccount[]): Map<string, EmojiEntry> { 79 const counts = new Map<string, EmojiEntry>(); 80 81 accounts.forEach((account) => { 82 getAccountEmojiKeys(account).forEach((key) => upsertEmojiEntry(counts, key)); 83 }); 84 85 return counts; 86 } 87 88 export function getSortedEmojis(counts: Map<string, EmojiEntry>, limit = 80): EmojiEntry[] { 89 return Array.from(counts.values()) 90 .sort((a, b) => b.count - a.count) 91 .slice(0, limit); 92 }