summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts/search-emoji.ts
blob: 4192a2df8fa65031df87f56f8b97553826c4411d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */

export type EmojiDef = {
	emoji: string;
	name: string;
	url: string;
	aliasOf?: string;
} | {
	emoji: string;
	name: string;
	aliasOf?: string;
	isCustomEmoji?: true;
};
type EmojiScore = { emoji: EmojiDef, score: number };

export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
	if (!query) {
		return [];
	}

	const matched = new Map<string, EmojiScore>();
	// 完全一致(エイリアスなし)
	emojiDb.some(x => {
		if (x.name.toLowerCase() === query && !x.aliasOf) {
			matched.set(x.name, { emoji: x, score: query.length + 3 });
		}
		return matched.size === max;
	});

	// 完全一致(エイリアス込み)
	if (matched.size < max) {
		emojiDb.some(x => {
			if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
			}
			return matched.size === max;
		});
	}

	// 前方一致(エイリアスなし)
	if (matched.size < max) {
		emojiDb.some(x => {
			if (x.name.toLowerCase().startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
				matched.set(x.name, { emoji: x, score: query.length + 1 });
			}
			return matched.size === max;
		});
	}

	// 前方一致(エイリアス込み)
	if (matched.size < max) {
		emojiDb.some(x => {
			if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
			}
			return matched.size === max;
		});
	}

	// 部分一致(エイリアス込み)
	if (matched.size < max) {
		emojiDb.some(x => {
			if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
			}
			return matched.size === max;
		});
	}

	// 簡易あいまい検索(3文字以上)
	if (matched.size < max && query.length > 3) {
		const queryChars = [...query];
		const hitEmojis = new Map<string, EmojiScore>();

		for (const x of emojiDb) {
			// 文字列の位置を進めながら、クエリの文字を順番に探す

			let pos = 0;
			let hit = 0;
			for (const c of queryChars) {
				pos = x.name.indexOf(c, pos);
				if (pos <= -1) break;
				hit++;
			}

			// 半分以上の文字が含まれていればヒットとする
			if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
				hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
			}
		}

		// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
		[...hitEmojis.values()]
			.sort((x, y) => y.score - x.score)
			.slice(0, 6)
			.forEach(it => matched.set(it.emoji.name, it));
	}

	return [...matched.values()]
		.sort((x, y) => y.score - x.score)
		.slice(0, max)
		.map(it => it.emoji);
}