summaryrefslogtreecommitdiff
path: root/packages/frontend/src/utility/search-emoji.ts
blob: 4cda880bff2368a1e8f83d693c93d4677d3e283c (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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/*
 * 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 === 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 === 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.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.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.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);
}

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

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

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