mirror of
https://hk.gh-proxy.com/https://github.com/NodeBB/nodebb-plugin-emoji.git
synced 2025-10-03 01:10:57 +08:00
Use a better search algorithm
- about 10x faster - 18kb smaller - more relevant results?
This commit is contained in:
parent
0bb3874316
commit
aafa4feb0b
9 changed files with 224 additions and 45 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -60,4 +60,5 @@ typings/
|
|||
|
||||
# Custom ignores
|
||||
build
|
||||
.vscode
|
||||
.vscode
|
||||
tmp
|
|
@ -62,3 +62,4 @@ typings/
|
|||
.git
|
||||
.gitignore
|
||||
.vscode
|
||||
tmp
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"dependencies": {
|
||||
"async": "^2.5.0",
|
||||
"fs-extra": "^4.0.1",
|
||||
"fuse.js": "^3.1.0",
|
||||
"lodash": "^4.17.4",
|
||||
"multer": "^1.3.0",
|
||||
"preact": "^8.2.5",
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"public/style.less"
|
||||
],
|
||||
"modules": {
|
||||
"Fuse.js": "node_modules/fuse.js/dist/fuse.js",
|
||||
"emoji.js": "build/public/lib/emoji.js",
|
||||
"emoji-dialog.js": "build/public/lib/emoji-dialog.js",
|
||||
"preact.js": "node_modules/preact/dist/preact.js",
|
||||
|
|
|
@ -1,6 +1,155 @@
|
|||
define('leven', function () {
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
var arr = [];
|
||||
var charCodeCache = [];
|
||||
|
||||
return function (a, b) {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var swap = a;
|
||||
|
||||
// Swapping the strings if `a` is longer than `b` so we know which one is the
|
||||
// shortest & which one is the longest
|
||||
if (a.length > b.length) {
|
||||
a = b;
|
||||
b = swap;
|
||||
}
|
||||
|
||||
var aLen = a.length;
|
||||
var bLen = b.length;
|
||||
|
||||
// Performing suffix trimming:
|
||||
// We can linearly drop suffix common to both strings since they
|
||||
// don't increase distance at all
|
||||
// Note: `~-` is the bitwise way to perform a `- 1` operation
|
||||
while (aLen > 0 && (a.charCodeAt(~-aLen) === b.charCodeAt(~-bLen))) {
|
||||
aLen--;
|
||||
bLen--;
|
||||
}
|
||||
|
||||
// Performing prefix trimming
|
||||
// We can linearly drop prefix common to both strings since they
|
||||
// don't increase distance at all
|
||||
var start = 0;
|
||||
|
||||
while (start < aLen && (a.charCodeAt(start) === b.charCodeAt(start))) {
|
||||
start++;
|
||||
}
|
||||
|
||||
aLen -= start;
|
||||
bLen -= start;
|
||||
|
||||
if (aLen === 0) {
|
||||
return bLen;
|
||||
}
|
||||
|
||||
var bCharCode;
|
||||
var ret;
|
||||
var tmp;
|
||||
var tmp2;
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
while (i < aLen) {
|
||||
charCodeCache[i] = a.charCodeAt(start + i);
|
||||
arr[i] = ++i;
|
||||
}
|
||||
|
||||
while (j < bLen) {
|
||||
bCharCode = b.charCodeAt(start + j);
|
||||
tmp = j++;
|
||||
ret = j;
|
||||
|
||||
for (i = 0; i < aLen; i++) {
|
||||
tmp2 = bCharCode === charCodeCache[i] ? tmp : tmp + 1;
|
||||
tmp = arr[i];
|
||||
ret = arr[i] = tmp > ret ? tmp2 > ret ? ret + 1 : tmp2 : tmp2 > tmp ? tmp + 1 : tmp2;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
define('fuzzysearch', function () {
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2015 Nicolas Bevacqua
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function fuzzysearch (needle, haystack) {
|
||||
var hlen = haystack.length;
|
||||
var nlen = needle.length;
|
||||
if (nlen > hlen) {
|
||||
return false;
|
||||
}
|
||||
if (nlen === hlen) {
|
||||
return needle === haystack;
|
||||
}
|
||||
outer: for (var i = 0, j = 0; i < nlen; i++) {
|
||||
var nch = needle.charCodeAt(i);
|
||||
while (j < hlen) {
|
||||
if (haystack.charCodeAt(j++) === nch) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return fuzzysearch;
|
||||
});
|
||||
|
||||
require(['emoji'], function (emoji) {
|
||||
$(window).on('composer:autocomplete:init chat:autocomplete:init', function (e, data) {
|
||||
emoji.init();
|
||||
data.strategies.push(emoji.strategy);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { h, Component, FunctionalComponent, render } from 'preact';
|
||||
import { fuse, strategy, table, buildEmoji, init as initEmoji } from 'emoji';
|
||||
import { strategy, table, buildEmoji, init as initEmoji } from 'emoji';
|
||||
|
||||
import 'preact/devtools';
|
||||
// import 'preact/devtools';
|
||||
|
||||
const setsEqual = (arr1: string[], arr2: string[]) => {
|
||||
if (arr1.length !== arr2.length) {
|
||||
|
|
|
@ -24,18 +24,12 @@ export function buildEmoji(emoji: StoredEmoji, defer?: boolean) {
|
|||
}
|
||||
|
||||
export let table: MetaData.table;
|
||||
export let fuse: Fuse<StoredEmoji>;
|
||||
export let search: (term: string) => StoredEmoji[];
|
||||
|
||||
export const strategy = {
|
||||
match: /\B:([^\s\n:]+)$/,
|
||||
search: (term: string, callback: Callback<StoredEmoji[]>) => {
|
||||
if (!term) {
|
||||
callback(Object.keys(table).map(key => table[key]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback(fuse.search(term));
|
||||
callback(search(term));
|
||||
},
|
||||
index: 1,
|
||||
replace: (emoji: StoredEmoji) => {
|
||||
|
@ -56,35 +50,67 @@ export function init(callback?: Callback) {
|
|||
initialized = true;
|
||||
|
||||
Promise.all([
|
||||
import('Fuse'),
|
||||
import('fuzzysearch'),
|
||||
import('leven'),
|
||||
import('composer/formatting'),
|
||||
Promise.resolve($.getJSON(`${base}/emoji/table.json?${buster}`)) as Promise<MetaData.table>,
|
||||
]).then(([Fuse, formatting, tableData]) => { // tslint:disable-line variable-name
|
||||
$.getJSON(`${base}/emoji/table.json?${buster}`) as PromiseLike<MetaData.table>,
|
||||
]).then(([fuzzy, leven, formatting, tableData]) => {
|
||||
table = tableData;
|
||||
const all = Object.keys(table).map(name => table[name]);
|
||||
|
||||
fuse = new Fuse(all, {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
keys: [
|
||||
{
|
||||
name: 'name',
|
||||
weight: 0.6,
|
||||
},
|
||||
{
|
||||
name: 'aliases',
|
||||
weight: 0.3,
|
||||
},
|
||||
{
|
||||
name: 'keywords',
|
||||
weight: 0.3,
|
||||
},
|
||||
],
|
||||
const all: (StoredEmoji & { score?: number })[] = Object.keys(table).map((name) => {
|
||||
const { aliases, character, image, keywords, pack } = table[name];
|
||||
return { name, aliases, character, image, keywords, pack };
|
||||
});
|
||||
|
||||
function fuzzyFind(term: string, arr: string[]) {
|
||||
const l = arr.length;
|
||||
|
||||
for (let i = 0; i < l; i += 1) {
|
||||
if (fuzzy(term, arr[i])) {
|
||||
return arr[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzySearch(term: string) {
|
||||
return all.filter((obj) => {
|
||||
if (fuzzy(term, obj.name)) {
|
||||
obj.score = leven(term, obj.name);
|
||||
if (obj.name.startsWith(term)) {
|
||||
obj.score -= 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const aliasMatch = fuzzyFind(term, obj.aliases);
|
||||
if (aliasMatch) {
|
||||
obj.score = 3 * leven(term, aliasMatch);
|
||||
if (aliasMatch.startsWith(term)) {
|
||||
obj.score -= 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const keywordMatch = fuzzyFind(term, obj.keywords);
|
||||
if (keywordMatch) {
|
||||
obj.score = 8 * leven(term, keywordMatch);
|
||||
if (keywordMatch.startsWith(term)) {
|
||||
obj.score -= 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).sort((a, b) => a.score - b.score).slice(0, 10);
|
||||
}
|
||||
|
||||
search = fuzzySearch;
|
||||
|
||||
formatting.addButtonDispatch(
|
||||
'emoji-add-emoji',
|
||||
(textarea: HTMLTextAreaElement) => {
|
||||
|
|
14
public/lib/types.d.ts
vendored
14
public/lib/types.d.ts
vendored
|
@ -18,6 +18,10 @@ interface Window {
|
|||
};
|
||||
}
|
||||
|
||||
interface String {
|
||||
startsWith(str: string): boolean;
|
||||
}
|
||||
|
||||
declare const socket: SocketIO.Server;
|
||||
|
||||
interface JQuery {
|
||||
|
@ -39,7 +43,11 @@ declare module 'composer/formatting' {
|
|||
declare module 'scrollStop' {
|
||||
export function apply(element: Element): void;
|
||||
}
|
||||
declare module 'Fuse' {
|
||||
import * as Fuse from 'fuse';
|
||||
export = Fuse;
|
||||
declare module 'fuzzysearch' {
|
||||
const fuzzysearch: (needle: string, haystack: string) => boolean;
|
||||
export = fuzzysearch;
|
||||
}
|
||||
declare module 'leven' {
|
||||
const leven: (a: string, b: string) => number;
|
||||
export = leven;
|
||||
}
|
||||
|
|
|
@ -230,10 +230,6 @@ fs.realpath@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
||||
fuse.js@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
|
||||
|
||||
glob@^7.1.1:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue