fuse.basic.esm.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261
  1. /**
  2. * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io)
  3. *
  4. * Copyright (c) 2022 Kiro Risk (http://kiro.me)
  5. * All Rights Reserved. Apache Software License 2.0
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. */
  9. function isArray(value) {
  10. return !Array.isArray
  11. ? getTag(value) === '[object Array]'
  12. : Array.isArray(value)
  13. }
  14. // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/baseToString.js
  15. const INFINITY = 1 / 0;
  16. function baseToString(value) {
  17. // Exit early for strings to avoid a performance hit in some environments.
  18. if (typeof value == 'string') {
  19. return value
  20. }
  21. let result = value + '';
  22. return result == '0' && 1 / value == -INFINITY ? '-0' : result
  23. }
  24. function toString(value) {
  25. return value == null ? '' : baseToString(value)
  26. }
  27. function isString(value) {
  28. return typeof value === 'string'
  29. }
  30. function isNumber(value) {
  31. return typeof value === 'number'
  32. }
  33. // Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js
  34. function isBoolean(value) {
  35. return (
  36. value === true ||
  37. value === false ||
  38. (isObjectLike(value) && getTag(value) == '[object Boolean]')
  39. )
  40. }
  41. function isObject(value) {
  42. return typeof value === 'object'
  43. }
  44. // Checks if `value` is object-like.
  45. function isObjectLike(value) {
  46. return isObject(value) && value !== null
  47. }
  48. function isDefined(value) {
  49. return value !== undefined && value !== null
  50. }
  51. function isBlank(value) {
  52. return !value.trim().length
  53. }
  54. // Gets the `toStringTag` of `value`.
  55. // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js
  56. function getTag(value) {
  57. return value == null
  58. ? value === undefined
  59. ? '[object Undefined]'
  60. : '[object Null]'
  61. : Object.prototype.toString.call(value)
  62. }
  63. const EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available';
  64. const LOGICAL_SEARCH_UNAVAILABLE = 'Logical search is not available';
  65. const INCORRECT_INDEX_TYPE = "Incorrect 'index' type";
  66. const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) =>
  67. `Invalid value for key ${key}`;
  68. const PATTERN_LENGTH_TOO_LARGE = (max) =>
  69. `Pattern length exceeds max of ${max}.`;
  70. const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`;
  71. const INVALID_KEY_WEIGHT_VALUE = (key) =>
  72. `Property 'weight' in key '${key}' must be a positive integer`;
  73. const hasOwn = Object.prototype.hasOwnProperty;
  74. class KeyStore {
  75. constructor(keys) {
  76. this._keys = [];
  77. this._keyMap = {};
  78. let totalWeight = 0;
  79. keys.forEach((key) => {
  80. let obj = createKey(key);
  81. totalWeight += obj.weight;
  82. this._keys.push(obj);
  83. this._keyMap[obj.id] = obj;
  84. totalWeight += obj.weight;
  85. });
  86. // Normalize weights so that their sum is equal to 1
  87. this._keys.forEach((key) => {
  88. key.weight /= totalWeight;
  89. });
  90. }
  91. get(keyId) {
  92. return this._keyMap[keyId]
  93. }
  94. keys() {
  95. return this._keys
  96. }
  97. toJSON() {
  98. return JSON.stringify(this._keys)
  99. }
  100. }
  101. function createKey(key) {
  102. let path = null;
  103. let id = null;
  104. let src = null;
  105. let weight = 1;
  106. let getFn = null;
  107. if (isString(key) || isArray(key)) {
  108. src = key;
  109. path = createKeyPath(key);
  110. id = createKeyId(key);
  111. } else {
  112. if (!hasOwn.call(key, 'name')) {
  113. throw new Error(MISSING_KEY_PROPERTY('name'))
  114. }
  115. const name = key.name;
  116. src = name;
  117. if (hasOwn.call(key, 'weight')) {
  118. weight = key.weight;
  119. if (weight <= 0) {
  120. throw new Error(INVALID_KEY_WEIGHT_VALUE(name))
  121. }
  122. }
  123. path = createKeyPath(name);
  124. id = createKeyId(name);
  125. getFn = key.getFn;
  126. }
  127. return { path, id, weight, src, getFn }
  128. }
  129. function createKeyPath(key) {
  130. return isArray(key) ? key : key.split('.')
  131. }
  132. function createKeyId(key) {
  133. return isArray(key) ? key.join('.') : key
  134. }
  135. function get(obj, path) {
  136. let list = [];
  137. let arr = false;
  138. const deepGet = (obj, path, index) => {
  139. if (!isDefined(obj)) {
  140. return
  141. }
  142. if (!path[index]) {
  143. // If there's no path left, we've arrived at the object we care about.
  144. list.push(obj);
  145. } else {
  146. let key = path[index];
  147. const value = obj[key];
  148. if (!isDefined(value)) {
  149. return
  150. }
  151. // If we're at the last value in the path, and if it's a string/number/bool,
  152. // add it to the list
  153. if (
  154. index === path.length - 1 &&
  155. (isString(value) || isNumber(value) || isBoolean(value))
  156. ) {
  157. list.push(toString(value));
  158. } else if (isArray(value)) {
  159. arr = true;
  160. // Search each item in the array.
  161. for (let i = 0, len = value.length; i < len; i += 1) {
  162. deepGet(value[i], path, index + 1);
  163. }
  164. } else if (path.length) {
  165. // An object. Recurse further.
  166. deepGet(value, path, index + 1);
  167. }
  168. }
  169. };
  170. // Backwards compatibility (since path used to be a string)
  171. deepGet(obj, isString(path) ? path.split('.') : path, 0);
  172. return arr ? list : list[0]
  173. }
  174. const MatchOptions = {
  175. // Whether the matches should be included in the result set. When `true`, each record in the result
  176. // set will include the indices of the matched characters.
  177. // These can consequently be used for highlighting purposes.
  178. includeMatches: false,
  179. // When `true`, the matching function will continue to the end of a search pattern even if
  180. // a perfect match has already been located in the string.
  181. findAllMatches: false,
  182. // Minimum number of characters that must be matched before a result is considered a match
  183. minMatchCharLength: 1
  184. };
  185. const BasicOptions = {
  186. // When `true`, the algorithm continues searching to the end of the input even if a perfect
  187. // match is found before the end of the same input.
  188. isCaseSensitive: false,
  189. // When true, the matching function will continue to the end of a search pattern even if
  190. includeScore: false,
  191. // List of properties that will be searched. This also supports nested properties.
  192. keys: [],
  193. // Whether to sort the result list, by score
  194. shouldSort: true,
  195. // Default sort function: sort by ascending score, ascending index
  196. sortFn: (a, b) =>
  197. a.score === b.score ? (a.idx < b.idx ? -1 : 1) : a.score < b.score ? -1 : 1
  198. };
  199. const FuzzyOptions = {
  200. // Approximately where in the text is the pattern expected to be found?
  201. location: 0,
  202. // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
  203. // (of both letters and location), a threshold of '1.0' would match anything.
  204. threshold: 0.6,
  205. // Determines how close the match must be to the fuzzy location (specified above).
  206. // An exact letter match which is 'distance' characters away from the fuzzy location
  207. // would score as a complete mismatch. A distance of '0' requires the match be at
  208. // the exact location specified, a threshold of '1000' would require a perfect match
  209. // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
  210. distance: 100
  211. };
  212. const AdvancedOptions = {
  213. // When `true`, it enables the use of unix-like search commands
  214. useExtendedSearch: false,
  215. // The get function to use when fetching an object's properties.
  216. // The default will search nested paths *ie foo.bar.baz*
  217. getFn: get,
  218. // When `true`, search will ignore `location` and `distance`, so it won't matter
  219. // where in the string the pattern appears.
  220. // More info: https://fusejs.io/concepts/scoring-theory.html#fuzziness-score
  221. ignoreLocation: false,
  222. // When `true`, the calculation for the relevance score (used for sorting) will
  223. // ignore the field-length norm.
  224. // More info: https://fusejs.io/concepts/scoring-theory.html#field-length-norm
  225. ignoreFieldNorm: false,
  226. // The weight to determine how much field length norm effects scoring.
  227. fieldNormWeight: 1
  228. };
  229. var Config = {
  230. ...BasicOptions,
  231. ...MatchOptions,
  232. ...FuzzyOptions,
  233. ...AdvancedOptions
  234. };
  235. const SPACE = /[^ ]+/g;
  236. // Field-length norm: the shorter the field, the higher the weight.
  237. // Set to 3 decimals to reduce index size.
  238. function norm(weight = 1, mantissa = 3) {
  239. const cache = new Map();
  240. const m = Math.pow(10, mantissa);
  241. return {
  242. get(value) {
  243. const numTokens = value.match(SPACE).length;
  244. if (cache.has(numTokens)) {
  245. return cache.get(numTokens)
  246. }
  247. // Default function is 1/sqrt(x), weight makes that variable
  248. const norm = 1 / Math.pow(numTokens, 0.5 * weight);
  249. // In place of `toFixed(mantissa)`, for faster computation
  250. const n = parseFloat(Math.round(norm * m) / m);
  251. cache.set(numTokens, n);
  252. return n
  253. },
  254. clear() {
  255. cache.clear();
  256. }
  257. }
  258. }
  259. class FuseIndex {
  260. constructor({
  261. getFn = Config.getFn,
  262. fieldNormWeight = Config.fieldNormWeight
  263. } = {}) {
  264. this.norm = norm(fieldNormWeight, 3);
  265. this.getFn = getFn;
  266. this.isCreated = false;
  267. this.setIndexRecords();
  268. }
  269. setSources(docs = []) {
  270. this.docs = docs;
  271. }
  272. setIndexRecords(records = []) {
  273. this.records = records;
  274. }
  275. setKeys(keys = []) {
  276. this.keys = keys;
  277. this._keysMap = {};
  278. keys.forEach((key, idx) => {
  279. this._keysMap[key.id] = idx;
  280. });
  281. }
  282. create() {
  283. if (this.isCreated || !this.docs.length) {
  284. return
  285. }
  286. this.isCreated = true;
  287. // List is Array<String>
  288. if (isString(this.docs[0])) {
  289. this.docs.forEach((doc, docIndex) => {
  290. this._addString(doc, docIndex);
  291. });
  292. } else {
  293. // List is Array<Object>
  294. this.docs.forEach((doc, docIndex) => {
  295. this._addObject(doc, docIndex);
  296. });
  297. }
  298. this.norm.clear();
  299. }
  300. // Adds a doc to the end of the index
  301. add(doc) {
  302. const idx = this.size();
  303. if (isString(doc)) {
  304. this._addString(doc, idx);
  305. } else {
  306. this._addObject(doc, idx);
  307. }
  308. }
  309. // Removes the doc at the specified index of the index
  310. removeAt(idx) {
  311. this.records.splice(idx, 1);
  312. // Change ref index of every subsquent doc
  313. for (let i = idx, len = this.size(); i < len; i += 1) {
  314. this.records[i].i -= 1;
  315. }
  316. }
  317. getValueForItemAtKeyId(item, keyId) {
  318. return item[this._keysMap[keyId]]
  319. }
  320. size() {
  321. return this.records.length
  322. }
  323. _addString(doc, docIndex) {
  324. if (!isDefined(doc) || isBlank(doc)) {
  325. return
  326. }
  327. let record = {
  328. v: doc,
  329. i: docIndex,
  330. n: this.norm.get(doc)
  331. };
  332. this.records.push(record);
  333. }
  334. _addObject(doc, docIndex) {
  335. let record = { i: docIndex, $: {} };
  336. // Iterate over every key (i.e, path), and fetch the value at that key
  337. this.keys.forEach((key, keyIndex) => {
  338. let value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path);
  339. if (!isDefined(value)) {
  340. return
  341. }
  342. if (isArray(value)) {
  343. let subRecords = [];
  344. const stack = [{ nestedArrIndex: -1, value }];
  345. while (stack.length) {
  346. const { nestedArrIndex, value } = stack.pop();
  347. if (!isDefined(value)) {
  348. continue
  349. }
  350. if (isString(value) && !isBlank(value)) {
  351. let subRecord = {
  352. v: value,
  353. i: nestedArrIndex,
  354. n: this.norm.get(value)
  355. };
  356. subRecords.push(subRecord);
  357. } else if (isArray(value)) {
  358. value.forEach((item, k) => {
  359. stack.push({
  360. nestedArrIndex: k,
  361. value: item
  362. });
  363. });
  364. } else ;
  365. }
  366. record.$[keyIndex] = subRecords;
  367. } else if (isString(value) && !isBlank(value)) {
  368. let subRecord = {
  369. v: value,
  370. n: this.norm.get(value)
  371. };
  372. record.$[keyIndex] = subRecord;
  373. }
  374. });
  375. this.records.push(record);
  376. }
  377. toJSON() {
  378. return {
  379. keys: this.keys,
  380. records: this.records
  381. }
  382. }
  383. }
  384. function createIndex(
  385. keys,
  386. docs,
  387. { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}
  388. ) {
  389. const myIndex = new FuseIndex({ getFn, fieldNormWeight });
  390. myIndex.setKeys(keys.map(createKey));
  391. myIndex.setSources(docs);
  392. myIndex.create();
  393. return myIndex
  394. }
  395. function parseIndex(
  396. data,
  397. { getFn = Config.getFn, fieldNormWeight = Config.fieldNormWeight } = {}
  398. ) {
  399. const { keys, records } = data;
  400. const myIndex = new FuseIndex({ getFn, fieldNormWeight });
  401. myIndex.setKeys(keys);
  402. myIndex.setIndexRecords(records);
  403. return myIndex
  404. }
  405. function computeScore$1(
  406. pattern,
  407. {
  408. errors = 0,
  409. currentLocation = 0,
  410. expectedLocation = 0,
  411. distance = Config.distance,
  412. ignoreLocation = Config.ignoreLocation
  413. } = {}
  414. ) {
  415. const accuracy = errors / pattern.length;
  416. if (ignoreLocation) {
  417. return accuracy
  418. }
  419. const proximity = Math.abs(expectedLocation - currentLocation);
  420. if (!distance) {
  421. // Dodge divide by zero error.
  422. return proximity ? 1.0 : accuracy
  423. }
  424. return accuracy + proximity / distance
  425. }
  426. function convertMaskToIndices(
  427. matchmask = [],
  428. minMatchCharLength = Config.minMatchCharLength
  429. ) {
  430. let indices = [];
  431. let start = -1;
  432. let end = -1;
  433. let i = 0;
  434. for (let len = matchmask.length; i < len; i += 1) {
  435. let match = matchmask[i];
  436. if (match && start === -1) {
  437. start = i;
  438. } else if (!match && start !== -1) {
  439. end = i - 1;
  440. if (end - start + 1 >= minMatchCharLength) {
  441. indices.push([start, end]);
  442. }
  443. start = -1;
  444. }
  445. }
  446. // (i-1 - start) + 1 => i - start
  447. if (matchmask[i - 1] && i - start >= minMatchCharLength) {
  448. indices.push([start, i - 1]);
  449. }
  450. return indices
  451. }
  452. // Machine word size
  453. const MAX_BITS = 32;
  454. function search(
  455. text,
  456. pattern,
  457. patternAlphabet,
  458. {
  459. location = Config.location,
  460. distance = Config.distance,
  461. threshold = Config.threshold,
  462. findAllMatches = Config.findAllMatches,
  463. minMatchCharLength = Config.minMatchCharLength,
  464. includeMatches = Config.includeMatches,
  465. ignoreLocation = Config.ignoreLocation
  466. } = {}
  467. ) {
  468. if (pattern.length > MAX_BITS) {
  469. throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS))
  470. }
  471. const patternLen = pattern.length;
  472. // Set starting location at beginning text and initialize the alphabet.
  473. const textLen = text.length;
  474. // Handle the case when location > text.length
  475. const expectedLocation = Math.max(0, Math.min(location, textLen));
  476. // Highest score beyond which we give up.
  477. let currentThreshold = threshold;
  478. // Is there a nearby exact match? (speedup)
  479. let bestLocation = expectedLocation;
  480. // Performance: only computer matches when the minMatchCharLength > 1
  481. // OR if `includeMatches` is true.
  482. const computeMatches = minMatchCharLength > 1 || includeMatches;
  483. // A mask of the matches, used for building the indices
  484. const matchMask = computeMatches ? Array(textLen) : [];
  485. let index;
  486. // Get all exact matches, here for speed up
  487. while ((index = text.indexOf(pattern, bestLocation)) > -1) {
  488. let score = computeScore$1(pattern, {
  489. currentLocation: index,
  490. expectedLocation,
  491. distance,
  492. ignoreLocation
  493. });
  494. currentThreshold = Math.min(score, currentThreshold);
  495. bestLocation = index + patternLen;
  496. if (computeMatches) {
  497. let i = 0;
  498. while (i < patternLen) {
  499. matchMask[index + i] = 1;
  500. i += 1;
  501. }
  502. }
  503. }
  504. // Reset the best location
  505. bestLocation = -1;
  506. let lastBitArr = [];
  507. let finalScore = 1;
  508. let binMax = patternLen + textLen;
  509. const mask = 1 << (patternLen - 1);
  510. for (let i = 0; i < patternLen; i += 1) {
  511. // Scan for the best match; each iteration allows for one more error.
  512. // Run a binary search to determine how far from the match location we can stray
  513. // at this error level.
  514. let binMin = 0;
  515. let binMid = binMax;
  516. while (binMin < binMid) {
  517. const score = computeScore$1(pattern, {
  518. errors: i,
  519. currentLocation: expectedLocation + binMid,
  520. expectedLocation,
  521. distance,
  522. ignoreLocation
  523. });
  524. if (score <= currentThreshold) {
  525. binMin = binMid;
  526. } else {
  527. binMax = binMid;
  528. }
  529. binMid = Math.floor((binMax - binMin) / 2 + binMin);
  530. }
  531. // Use the result from this iteration as the maximum for the next.
  532. binMax = binMid;
  533. let start = Math.max(1, expectedLocation - binMid + 1);
  534. let finish = findAllMatches
  535. ? textLen
  536. : Math.min(expectedLocation + binMid, textLen) + patternLen;
  537. // Initialize the bit array
  538. let bitArr = Array(finish + 2);
  539. bitArr[finish + 1] = (1 << i) - 1;
  540. for (let j = finish; j >= start; j -= 1) {
  541. let currentLocation = j - 1;
  542. let charMatch = patternAlphabet[text.charAt(currentLocation)];
  543. if (computeMatches) {
  544. // Speed up: quick bool to int conversion (i.e, `charMatch ? 1 : 0`)
  545. matchMask[currentLocation] = +!!charMatch;
  546. }
  547. // First pass: exact match
  548. bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch;
  549. // Subsequent passes: fuzzy match
  550. if (i) {
  551. bitArr[j] |=
  552. ((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1 | lastBitArr[j + 1];
  553. }
  554. if (bitArr[j] & mask) {
  555. finalScore = computeScore$1(pattern, {
  556. errors: i,
  557. currentLocation,
  558. expectedLocation,
  559. distance,
  560. ignoreLocation
  561. });
  562. // This match will almost certainly be better than any existing match.
  563. // But check anyway.
  564. if (finalScore <= currentThreshold) {
  565. // Indeed it is
  566. currentThreshold = finalScore;
  567. bestLocation = currentLocation;
  568. // Already passed `loc`, downhill from here on in.
  569. if (bestLocation <= expectedLocation) {
  570. break
  571. }
  572. // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
  573. start = Math.max(1, 2 * expectedLocation - bestLocation);
  574. }
  575. }
  576. }
  577. // No hope for a (better) match at greater error levels.
  578. const score = computeScore$1(pattern, {
  579. errors: i + 1,
  580. currentLocation: expectedLocation,
  581. expectedLocation,
  582. distance,
  583. ignoreLocation
  584. });
  585. if (score > currentThreshold) {
  586. break
  587. }
  588. lastBitArr = bitArr;
  589. }
  590. const result = {
  591. isMatch: bestLocation >= 0,
  592. // Count exact matches (those with a score of 0) to be "almost" exact
  593. score: Math.max(0.001, finalScore)
  594. };
  595. if (computeMatches) {
  596. const indices = convertMaskToIndices(matchMask, minMatchCharLength);
  597. if (!indices.length) {
  598. result.isMatch = false;
  599. } else if (includeMatches) {
  600. result.indices = indices;
  601. }
  602. }
  603. return result
  604. }
  605. function createPatternAlphabet(pattern) {
  606. let mask = {};
  607. for (let i = 0, len = pattern.length; i < len; i += 1) {
  608. const char = pattern.charAt(i);
  609. mask[char] = (mask[char] || 0) | (1 << (len - i - 1));
  610. }
  611. return mask
  612. }
  613. class BitapSearch {
  614. constructor(
  615. pattern,
  616. {
  617. location = Config.location,
  618. threshold = Config.threshold,
  619. distance = Config.distance,
  620. includeMatches = Config.includeMatches,
  621. findAllMatches = Config.findAllMatches,
  622. minMatchCharLength = Config.minMatchCharLength,
  623. isCaseSensitive = Config.isCaseSensitive,
  624. ignoreLocation = Config.ignoreLocation
  625. } = {}
  626. ) {
  627. this.options = {
  628. location,
  629. threshold,
  630. distance,
  631. includeMatches,
  632. findAllMatches,
  633. minMatchCharLength,
  634. isCaseSensitive,
  635. ignoreLocation
  636. };
  637. this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
  638. this.chunks = [];
  639. if (!this.pattern.length) {
  640. return
  641. }
  642. const addChunk = (pattern, startIndex) => {
  643. this.chunks.push({
  644. pattern,
  645. alphabet: createPatternAlphabet(pattern),
  646. startIndex
  647. });
  648. };
  649. const len = this.pattern.length;
  650. if (len > MAX_BITS) {
  651. let i = 0;
  652. const remainder = len % MAX_BITS;
  653. const end = len - remainder;
  654. while (i < end) {
  655. addChunk(this.pattern.substr(i, MAX_BITS), i);
  656. i += MAX_BITS;
  657. }
  658. if (remainder) {
  659. const startIndex = len - MAX_BITS;
  660. addChunk(this.pattern.substr(startIndex), startIndex);
  661. }
  662. } else {
  663. addChunk(this.pattern, 0);
  664. }
  665. }
  666. searchIn(text) {
  667. const { isCaseSensitive, includeMatches } = this.options;
  668. if (!isCaseSensitive) {
  669. text = text.toLowerCase();
  670. }
  671. // Exact match
  672. if (this.pattern === text) {
  673. let result = {
  674. isMatch: true,
  675. score: 0
  676. };
  677. if (includeMatches) {
  678. result.indices = [[0, text.length - 1]];
  679. }
  680. return result
  681. }
  682. // Otherwise, use Bitap algorithm
  683. const {
  684. location,
  685. distance,
  686. threshold,
  687. findAllMatches,
  688. minMatchCharLength,
  689. ignoreLocation
  690. } = this.options;
  691. let allIndices = [];
  692. let totalScore = 0;
  693. let hasMatches = false;
  694. this.chunks.forEach(({ pattern, alphabet, startIndex }) => {
  695. const { isMatch, score, indices } = search(text, pattern, alphabet, {
  696. location: location + startIndex,
  697. distance,
  698. threshold,
  699. findAllMatches,
  700. minMatchCharLength,
  701. includeMatches,
  702. ignoreLocation
  703. });
  704. if (isMatch) {
  705. hasMatches = true;
  706. }
  707. totalScore += score;
  708. if (isMatch && indices) {
  709. allIndices = [...allIndices, ...indices];
  710. }
  711. });
  712. let result = {
  713. isMatch: hasMatches,
  714. score: hasMatches ? totalScore / this.chunks.length : 1
  715. };
  716. if (hasMatches && includeMatches) {
  717. result.indices = allIndices;
  718. }
  719. return result
  720. }
  721. }
  722. const registeredSearchers = [];
  723. function createSearcher(pattern, options) {
  724. for (let i = 0, len = registeredSearchers.length; i < len; i += 1) {
  725. let searcherClass = registeredSearchers[i];
  726. if (searcherClass.condition(pattern, options)) {
  727. return new searcherClass(pattern, options)
  728. }
  729. }
  730. return new BitapSearch(pattern, options)
  731. }
  732. const LogicalOperator = {
  733. AND: '$and',
  734. OR: '$or'
  735. };
  736. const KeyType = {
  737. PATH: '$path',
  738. PATTERN: '$val'
  739. };
  740. const isExpression = (query) =>
  741. !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]);
  742. const isPath = (query) => !!query[KeyType.PATH];
  743. const isLeaf = (query) =>
  744. !isArray(query) && isObject(query) && !isExpression(query);
  745. const convertToExplicit = (query) => ({
  746. [LogicalOperator.AND]: Object.keys(query).map((key) => ({
  747. [key]: query[key]
  748. }))
  749. });
  750. // When `auto` is `true`, the parse function will infer and initialize and add
  751. // the appropriate `Searcher` instance
  752. function parse(query, options, { auto = true } = {}) {
  753. const next = (query) => {
  754. let keys = Object.keys(query);
  755. const isQueryPath = isPath(query);
  756. if (!isQueryPath && keys.length > 1 && !isExpression(query)) {
  757. return next(convertToExplicit(query))
  758. }
  759. if (isLeaf(query)) {
  760. const key = isQueryPath ? query[KeyType.PATH] : keys[0];
  761. const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key];
  762. if (!isString(pattern)) {
  763. throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key))
  764. }
  765. const obj = {
  766. keyId: createKeyId(key),
  767. pattern
  768. };
  769. if (auto) {
  770. obj.searcher = createSearcher(pattern, options);
  771. }
  772. return obj
  773. }
  774. let node = {
  775. children: [],
  776. operator: keys[0]
  777. };
  778. keys.forEach((key) => {
  779. const value = query[key];
  780. if (isArray(value)) {
  781. value.forEach((item) => {
  782. node.children.push(next(item));
  783. });
  784. }
  785. });
  786. return node
  787. };
  788. if (!isExpression(query)) {
  789. query = convertToExplicit(query);
  790. }
  791. return next(query)
  792. }
  793. // Practical scoring function
  794. function computeScore(
  795. results,
  796. { ignoreFieldNorm = Config.ignoreFieldNorm }
  797. ) {
  798. results.forEach((result) => {
  799. let totalScore = 1;
  800. result.matches.forEach(({ key, norm, score }) => {
  801. const weight = key ? key.weight : null;
  802. totalScore *= Math.pow(
  803. score === 0 && weight ? Number.EPSILON : score,
  804. (weight || 1) * (ignoreFieldNorm ? 1 : norm)
  805. );
  806. });
  807. result.score = totalScore;
  808. });
  809. }
  810. function transformMatches(result, data) {
  811. const matches = result.matches;
  812. data.matches = [];
  813. if (!isDefined(matches)) {
  814. return
  815. }
  816. matches.forEach((match) => {
  817. if (!isDefined(match.indices) || !match.indices.length) {
  818. return
  819. }
  820. const { indices, value } = match;
  821. let obj = {
  822. indices,
  823. value
  824. };
  825. if (match.key) {
  826. obj.key = match.key.src;
  827. }
  828. if (match.idx > -1) {
  829. obj.refIndex = match.idx;
  830. }
  831. data.matches.push(obj);
  832. });
  833. }
  834. function transformScore(result, data) {
  835. data.score = result.score;
  836. }
  837. function format(
  838. results,
  839. docs,
  840. {
  841. includeMatches = Config.includeMatches,
  842. includeScore = Config.includeScore
  843. } = {}
  844. ) {
  845. const transformers = [];
  846. if (includeMatches) transformers.push(transformMatches);
  847. if (includeScore) transformers.push(transformScore);
  848. return results.map((result) => {
  849. const { idx } = result;
  850. const data = {
  851. item: docs[idx],
  852. refIndex: idx
  853. };
  854. if (transformers.length) {
  855. transformers.forEach((transformer) => {
  856. transformer(result, data);
  857. });
  858. }
  859. return data
  860. })
  861. }
  862. class Fuse {
  863. constructor(docs, options = {}, index) {
  864. this.options = { ...Config, ...options };
  865. if (
  866. this.options.useExtendedSearch &&
  867. !false
  868. ) {
  869. throw new Error(EXTENDED_SEARCH_UNAVAILABLE)
  870. }
  871. this._keyStore = new KeyStore(this.options.keys);
  872. this.setCollection(docs, index);
  873. }
  874. setCollection(docs, index) {
  875. this._docs = docs;
  876. if (index && !(index instanceof FuseIndex)) {
  877. throw new Error(INCORRECT_INDEX_TYPE)
  878. }
  879. this._myIndex =
  880. index ||
  881. createIndex(this.options.keys, this._docs, {
  882. getFn: this.options.getFn,
  883. fieldNormWeight: this.options.fieldNormWeight
  884. });
  885. }
  886. add(doc) {
  887. if (!isDefined(doc)) {
  888. return
  889. }
  890. this._docs.push(doc);
  891. this._myIndex.add(doc);
  892. }
  893. remove(predicate = (/* doc, idx */) => false) {
  894. const results = [];
  895. for (let i = 0, len = this._docs.length; i < len; i += 1) {
  896. const doc = this._docs[i];
  897. if (predicate(doc, i)) {
  898. this.removeAt(i);
  899. i -= 1;
  900. len -= 1;
  901. results.push(doc);
  902. }
  903. }
  904. return results
  905. }
  906. removeAt(idx) {
  907. this._docs.splice(idx, 1);
  908. this._myIndex.removeAt(idx);
  909. }
  910. getIndex() {
  911. return this._myIndex
  912. }
  913. search(query, { limit = -1 } = {}) {
  914. const {
  915. includeMatches,
  916. includeScore,
  917. shouldSort,
  918. sortFn,
  919. ignoreFieldNorm
  920. } = this.options;
  921. let results = isString(query)
  922. ? isString(this._docs[0])
  923. ? this._searchStringList(query)
  924. : this._searchObjectList(query)
  925. : this._searchLogical(query);
  926. computeScore(results, { ignoreFieldNorm });
  927. if (shouldSort) {
  928. results.sort(sortFn);
  929. }
  930. if (isNumber(limit) && limit > -1) {
  931. results = results.slice(0, limit);
  932. }
  933. return format(results, this._docs, {
  934. includeMatches,
  935. includeScore
  936. })
  937. }
  938. _searchStringList(query) {
  939. const searcher = createSearcher(query, this.options);
  940. const { records } = this._myIndex;
  941. const results = [];
  942. // Iterate over every string in the index
  943. records.forEach(({ v: text, i: idx, n: norm }) => {
  944. if (!isDefined(text)) {
  945. return
  946. }
  947. const { isMatch, score, indices } = searcher.searchIn(text);
  948. if (isMatch) {
  949. results.push({
  950. item: text,
  951. idx,
  952. matches: [{ score, value: text, norm, indices }]
  953. });
  954. }
  955. });
  956. return results
  957. }
  958. _searchLogical(query) {
  959. {
  960. throw new Error(LOGICAL_SEARCH_UNAVAILABLE)
  961. }
  962. }
  963. _searchObjectList(query) {
  964. const searcher = createSearcher(query, this.options);
  965. const { keys, records } = this._myIndex;
  966. const results = [];
  967. // List is Array<Object>
  968. records.forEach(({ $: item, i: idx }) => {
  969. if (!isDefined(item)) {
  970. return
  971. }
  972. let matches = [];
  973. // Iterate over every key (i.e, path), and fetch the value at that key
  974. keys.forEach((key, keyIndex) => {
  975. matches.push(
  976. ...this._findMatches({
  977. key,
  978. value: item[keyIndex],
  979. searcher
  980. })
  981. );
  982. });
  983. if (matches.length) {
  984. results.push({
  985. idx,
  986. item,
  987. matches
  988. });
  989. }
  990. });
  991. return results
  992. }
  993. _findMatches({ key, value, searcher }) {
  994. if (!isDefined(value)) {
  995. return []
  996. }
  997. let matches = [];
  998. if (isArray(value)) {
  999. value.forEach(({ v: text, i: idx, n: norm }) => {
  1000. if (!isDefined(text)) {
  1001. return
  1002. }
  1003. const { isMatch, score, indices } = searcher.searchIn(text);
  1004. if (isMatch) {
  1005. matches.push({
  1006. score,
  1007. key,
  1008. value: text,
  1009. idx,
  1010. norm,
  1011. indices
  1012. });
  1013. }
  1014. });
  1015. } else {
  1016. const { v: text, n: norm } = value;
  1017. const { isMatch, score, indices } = searcher.searchIn(text);
  1018. if (isMatch) {
  1019. matches.push({ score, key, value: text, norm, indices });
  1020. }
  1021. }
  1022. return matches
  1023. }
  1024. }
  1025. Fuse.version = '6.6.2';
  1026. Fuse.createIndex = createIndex;
  1027. Fuse.parseIndex = parseIndex;
  1028. Fuse.config = Config;
  1029. {
  1030. Fuse.parseQuery = parse;
  1031. }
  1032. export { Fuse as default };