lib/fuzzy.js (1753 bytes)
1 // Search as in fzf. Each word of the query must occur in the text, with its 2 // letters in order but not always together. Case does not matter. 3 "use strict"; 4 5 const Fuzzy = (() => { 6 const letterOrDigit = /[\p{L}\p{N}]/u; 7 8 function startsWord(hay, i) { 9 return i === 0 || !letterOrDigit.test(hay[i - 1]); 10 } 11 12 function scoreWord(word, hay) { 13 // The whole word in one place scores highest, most at the start of a word. 14 const at = hay.indexOf(word); 15 if (at >= 0) return 100 + word.length + (startsWord(hay, at) ? 20 : 0); 16 let score = 0; 17 let previous = -2; 18 let from = 0; 19 for (const c of word) { 20 const i = hay.indexOf(c, from); 21 if (i < 0) return null; 22 score += 1; 23 if (i === previous + 1) score += 5; 24 if (startsWord(hay, i)) score += 3; 25 previous = i; 26 from = i + c.length; 27 } 28 return score; 29 } 30 31 return { 32 /** The score of query in text, or null when a word does not occur. Higher is better. */ 33 score(query, text) { 34 const hay = text.toLowerCase(); 35 let total = 0; 36 for (const word of query.toLowerCase().split(/[ \t]/)) { 37 if (word === "") continue; 38 const s = scoreWord(word, hay); 39 if (s === null) return null; 40 total += s; 41 } 42 return total; 43 }, 44 45 /** The items that match query, best first. Items with the same score keep their order. */ 46 filter(items, query, text) { 47 if (query.trim() === "") return items; 48 return items 49 .map((item, i) => ({ item, i, s: Fuzzy.score(query, text(item)) })) 50 .filter((x) => x.s !== null) 51 .sort((a, b) => b.s - a.s || a.i - b.i) 52 .map((x) => x.item); 53 }, 54 }; 55 })(); 56 57 if (typeof module !== "undefined") module.exports = Fuzzy;