diff --git a/.gitignore b/.gitignore index 7831451..c74eeb1 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ typings/ # Custom ignores build -.vscode \ No newline at end of file +.vscode +tmp \ No newline at end of file diff --git a/.npmignore b/.npmignore index fd31bdc..834cc6d 100644 --- a/.npmignore +++ b/.npmignore @@ -62,3 +62,4 @@ typings/ .git .gitignore .vscode +tmp diff --git a/package.json b/package.json index c29bf67..9afbf4f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugin.json b/plugin.json index b3bdec0..3f3e656 100644 --- a/plugin.json +++ b/plugin.json @@ -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", diff --git a/public/emoji-setup.js b/public/emoji-setup.js index 107c999..166d14f 100644 --- a/public/emoji-setup.js +++ b/public/emoji-setup.js @@ -1,6 +1,155 @@ +define('leven', function () { + /* + The MIT License (MIT) + + Copyright (c) Sindre Sorhus (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); }); -}); \ No newline at end of file +}); diff --git a/public/lib/admin/custom-emoji.tsx b/public/lib/admin/custom-emoji.tsx index c898948..067c065 100644 --- a/public/lib/admin/custom-emoji.tsx +++ b/public/lib/admin/custom-emoji.tsx @@ -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) { diff --git a/public/lib/emoji.ts b/public/lib/emoji.ts index 06c67ad..7445d5e 100644 --- a/public/lib/emoji.ts +++ b/public/lib/emoji.ts @@ -24,18 +24,12 @@ export function buildEmoji(emoji: StoredEmoji, defer?: boolean) { } export let table: MetaData.table; -export let fuse: Fuse; +export let search: (term: string) => StoredEmoji[]; export const strategy = { match: /\B:([^\s\n:]+)$/, search: (term: string, callback: Callback) => { - 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, - ]).then(([Fuse, formatting, tableData]) => { // tslint:disable-line variable-name + $.getJSON(`${base}/emoji/table.json?${buster}`) as PromiseLike, + ]).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) => { diff --git a/public/lib/types.d.ts b/public/lib/types.d.ts index c97c175..f79cf0a 100644 --- a/public/lib/types.d.ts +++ b/public/lib/types.d.ts @@ -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; } diff --git a/yarn.lock b/yarn.lock index 05b2643..1be2f35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"