/ src / lib / utils / emoji.ts
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  }