2024-03-02 17:36:49 +01:00
|
|
|
|
/*
|
|
|
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
*/
|
|
|
|
|
|
2024-02-23 09:01:42 +01:00
|
|
|
|
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 => {
|
2024-02-23 13:42:52 +01:00
|
|
|
|
if (x.name.toLowerCase() === query && !x.aliasOf) {
|
2024-02-23 09:01:42 +01:00
|
|
|
|
matched.set(x.name, { emoji: x, score: query.length + 3 });
|
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 完全一致(エイリアス込み)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2024-02-23 13:42:52 +01:00
|
|
|
|
if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
|
2024-02-23 09:01:42 +01:00
|
|
|
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
|
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 前方一致(エイリアスなし)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2024-02-23 13:42:52 +01:00
|
|
|
|
if (x.name.toLowerCase().startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
|
2024-02-23 09:01:42 +01:00
|
|
|
|
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 前方一致(エイリアス込み)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2024-02-23 13:42:52 +01:00
|
|
|
|
if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
2024-02-23 09:01:42 +01:00
|
|
|
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 部分一致(エイリアス込み)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2024-02-23 13:42:52 +01:00
|
|
|
|
if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
2024-02-23 09:01:42 +01:00
|
|
|
|
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);
|
|
|
|
|
}
|