Browse Source

Changed bundler to Rollup (#367)

- Changed bundler to Rollup
- Added ES6 modules for bundlers and browsers (fixed #262)
- Added CommonJS builds
- Added UMD builds
- Name change so that .min actually reflects the minified version
Kiro Risk 2 years ago
parent
commit
e74a9ef305
68 changed files with 9005 additions and 9605 deletions
  1. 1 1
      .babelrc
  2. 0 1
      .gitignore
  3. 12 0
      README.md
  4. 0 55
      configs/webpack.common.js
  5. 0 10
      configs/webpack.dev.js
  6. 0 7
      configs/webpack.output.js
  7. 0 16
      configs/webpack.prod.js
  8. 0 17
      configs/webpack.raw.js
  9. 14 0
      dist/README.md
  10. 388 717
      dist/fuse.common.js
  11. 188 47
      dist/fuse.d.ts
  12. 0 1988
      dist/fuse.dev.js
  13. 1242 0
      dist/fuse.esm.js
  14. 4 4
      dist/fuse.js
  15. 8 0
      dist/fuse.min.js
  16. 0 0
      dist/fuse.raw.js.map
  17. 1 1
      jest.config.js
  18. 25 16
      package.json
  19. 83 0
      scripts/build.main.js
  20. 120 0
      scripts/configs.js
  21. 3 0
      scripts/feature-flags.js
  22. 10 5
      scripts/release.sh
  23. 284 0
      src/core/index.js
  24. 3 3
      src/helpers/get.js
  25. 7 16
      src/helpers/type-checkers.js
  26. 221 0
      src/index.d.ts
  27. 4 366
      src/index.js
  28. 1 1
      src/search/bitap-search/bitap-matched-indices.js
  29. 1 1
      src/search/bitap-search/bitap-pattern-alphabet.js
  30. 1 1
      src/search/bitap-search/bitap-score.js
  31. 3 3
      src/search/bitap-search/bitap-search.js
  32. 1 1
      src/search/bitap-search/constants.js
  33. 4 6
      src/search/bitap-search/index.js
  34. 1 1
      src/search/extended-search/exact-match.js
  35. 10 12
      src/search/extended-search/index.js
  36. 1 1
      src/search/extended-search/inverse-exact-match.js
  37. 1 1
      src/search/extended-search/inverse-prefix-exact-match.js
  38. 1 1
      src/search/extended-search/inverse-suffix-exact-match.js
  39. 1 1
      src/search/extended-search/prefix-exact-match.js
  40. 1 1
      src/search/extended-search/suffix-exact-match.js
  41. 8 4
      src/search/index.js
  42. 6 3
      src/search/ngram-search/array-utils/index.js
  43. 1 1
      src/search/ngram-search/array-utils/intersection.js
  44. 1 1
      src/search/ngram-search/array-utils/union.js
  45. 4 2
      src/search/ngram-search/distance/index.js
  46. 2 2
      src/search/ngram-search/distance/jaccard-distance.js
  47. 4 6
      src/search/ngram-search/index.js
  48. 2 2
      src/search/ngram-search/ngram.js
  49. 7 4
      src/tools/create-index.js
  50. 6 3
      src/tools/index.js
  51. 3 5
      src/tools/key-store.js
  52. 6 3
      src/transform/index.js
  53. 2 2
      src/transform/transform-matches.js
  54. 1 1
      src/transform/transform-score.js
  55. 0 80
      src/typings.d.ts
  56. 0 5
      test/base.js
  57. 0 5
      test/base.t.ts
  58. 1 2
      test/enhanced-search.test.js
  59. 0 106
      test/fixtures/books.json
  60. 68 0
      test/fixtures/pokedex.js
  61. 7 7
      test/fixtures/pokedex.ts
  62. 5 0
      test/fixtures/types.js
  63. 3 3
      test/fixtures/types.ts
  64. 1 2
      test/fuzzy-search.test.js
  65. 0 74
      test/typings.t.ts
  66. 43 0
      test/typings.test.ts
  67. 14 10
      tsconfig.json
  68. 6165 5971
      yarn.lock

+ 1 - 1
.babelrc

@@ -1,3 +1,3 @@
 {
   "presets": ["@babel/preset-env", "@babel/preset-typescript"]
-}
+}

+ 0 - 1
.gitignore

@@ -14,6 +14,5 @@ node_modules/
 package-lock.json
 
 # Filter misc
-runner.js
 playground/
 Makefile

+ 12 - 0
README.md

@@ -25,6 +25,10 @@ Through contributions, donations, and sponsorship, you allow Fuse.js to thrive.
 
 Fuse.js is a lightweight fuzzy-search, in JavaScript, with zero dependencies.
 
+### Browser Compatibility
+
+Fuse.js supports all browsers that are ES5-compliant (IE8 and below are not supported).
+
 ## Documentation
 
 To checkout out live examples and docs, visit [fusejs.io](https://fusejs.io).
@@ -43,6 +47,14 @@ $ npm install --save fuse.js
 $ yarn add fuse.js
 ```
 
+**CDN**
+
+Available on CDN via [jsDelivr](https://cdn.jsdelivr.net/npm/fuse.js/dist/).
+
+### Explanation of Different Builds
+
+In the [`dist/` directory of the NPM package](https://cdn.jsdelivr.net/npm/fuse.js/dist/) you will find many different builds of Fuse.js. Here's an [overview](dist/README.md) of the difference between them.
+
 ## Issues
 
 This repository serves as [the main issue tracker](https://github.com/krisk/Fuse/issues). When creating issues, it's important to follow common guidelines to make them extra clear. Here is a few links to help you achieve that:

+ 0 - 55
configs/webpack.common.js

@@ -1,55 +0,0 @@
-const webpack = require('webpack')
-const path = require('path')
-const fs = require('fs')
-const pckg = require('../package.json')
-const CopyPlugin = require('copy-webpack-plugin')
-
-const LIBRARY_NAME = 'fuse'
-const VERSION = pckg.version
-const AUTHOR = pckg.author
-const HOMEPAGE = pckg.homepage
-
-const copyright = fs.readFileSync(path.resolve(__dirname, '../COPYRIGHT.txt'), 'UTF8')
-const banner = copyright
-  .replace('{VERSION}', `v${VERSION}`)
-  .replace('{AUTHOR_URL}', `${AUTHOR.url}`)
-  .replace('{HOMEPAGE}', `${HOMEPAGE}`)
-
-global.LIBRARY_NAME = LIBRARY_NAME
-
-module.exports = {
-  entry: path.resolve(__dirname, '../src/index.js'),
-  module: {
-    rules: [{
-      test: /\.?(j|t)s$/,
-      exclude: /(node_modules)/,
-      use: {
-        loader: 'babel-loader',
-        options: {
-          presets: ['@babel/preset-env', '@babel/preset-typescript']
-        }
-      }
-    }]
-  },
-  plugins: [
-    new CopyPlugin([{
-      from: path.resolve(__dirname, '../src/typings.d.ts'),
-      to: path.resolve(__dirname, '../dist/fuse.d.ts'),
-      transform (content, path) {
-        return `// Type definitions for Fuse.js v${VERSION}\n${content}`
-      }
-    }]),
-    new webpack.BannerPlugin({
-      banner,
-      entryOnly: true
-    })
-  ],
-  output: {
-    path: path.resolve(__dirname, '../dist'),
-    filename: `${LIBRARY_NAME}.js`,
-    library: 'Fuse',
-    libraryTarget: 'umd',
-    umdNamedDefine: true,
-    globalObject: 'this'
-  }
-}

+ 0 - 10
configs/webpack.dev.js

@@ -1,10 +0,0 @@
-const merge = require('webpack-merge')
-const common = require('./webpack.common.js')
-
-module.exports = merge(common, {
-  mode: 'development',
-  devtool: 'inline-source-map',
-  output: {
-    filename: `${LIBRARY_NAME}.dev.js`
-  }
-})

+ 0 - 7
configs/webpack.output.js

@@ -1,7 +0,0 @@
-const prod = require('./webpack.prod.js')
-const dev = require('./webpack.dev.js')
-const raw = require('./webpack.raw.js')
-
-module.exports = [
-  raw, dev, prod
-]

+ 0 - 16
configs/webpack.prod.js

@@ -1,16 +0,0 @@
-const merge = require('webpack-merge')
-const common = require('./webpack.common.js')
-const TerserPlugin = require('terser-webpack-plugin')
-
-module.exports = merge(common, {
-  mode: 'production',
-  devtool: false,
-  optimization: {
-    minimizer: [new TerserPlugin({
-      extractComments: false
-    })]
-  },
-  output: {
-    filename: `${LIBRARY_NAME}.js`
-  }
-})

+ 0 - 17
configs/webpack.raw.js

@@ -1,17 +0,0 @@
-const webpack = require('webpack')
-const merge = require('webpack-merge')
-const common = require('./webpack.common.js')
-
-module.exports = merge(common, {
-  mode: 'none',
-  devtool: false,
-  plugins: [
-    new webpack.SourceMapDevToolPlugin({
-      filename: '[file].map'
-    })
-  ],
-  output: {
-    filename: `${LIBRARY_NAME}.raw.js`
-  }
-})
-

+ 14 - 0
dist/README.md

@@ -0,0 +1,14 @@
+## Explanation of Build Files
+
+|                 | UMD         | CommonJS       | ES Module (for bundlers) |
+| --------------- | ----------- | -------------- | ------------------------ |
+| **Development** | fuse.js     | fuse.common.js | fuse.esm.js              |
+| **Production**  | fuse.min.js | -              | -                        |
+
+### Terms
+
+- **[UMD](https://github.com/umdjs/umd)**: UMD builds can be used directly in the browser via a `<script>` tag. The default file from jsDelivr CDN at https://cdn.jsdelivr.net/npm/fuse.js is the UMD build (`fuse.js`).
+
+- **[CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1)**: CommonJS builds are intended for use with older bundlers like [browserify](http://browserify.org/) or [webpack 1](https://webpack.github.io). The file for these bundlers (`pkg.main`) is the CommonJS build (`fuse.common.js`).
+
+- **[ES Module](http://exploringjs.com/es6/ch_modules.html)**: Intended for use with modern bundlers like [Webpack 2](https://webpack.js.org) or [Rollup](http://rollupjs.org/). The file for these bundlers (`pkg.module`) is the ES Module build (`fuse.esm.js`).

File diff suppressed because it is too large
+ 388 - 717
dist/fuse.common.js


+ 188 - 47
dist/fuse.d.ts

@@ -1,81 +1,222 @@
 // Type definitions for Fuse.js v5.0.7-beta
-// TypeScript Version: 3.1
-
 export = Fuse
 export as namespace Fuse
 
-interface SearchOpt {
-  limit?: number
-}
-
-// TODO: Needs more work to actually make sense in TypeScript
-interface FuseIndexRecord {
-  idx: number
-  $: any
-}
-
-declare class Fuse<T, O extends Fuse.FuseOptions<T>> {
+declare class Fuse<T, O extends Fuse.IFuseOptions<T>> {
   constructor(
     list: ReadonlyArray<T>,
     options?: O,
-    index?: ReadonlyArray<FuseIndexRecord>,
+    index?: ReadonlyArray<Fuse.FuseIndexRecord>,
   )
-  search<
-    /** Type of item of return */
-    R = T,
-    /** include score (boolean) */
-    S = O['includeScore'],
-    /** include matches (boolean) */
-    M = O['includeMatches']
-  >(
+  /**
+   * Search function for the Fuse instance.
+   *
+   * ```typescript
+   * const list: MyType[] = [myType1, myType2, etc...]
+
+   * const options: Fuse.IFuseOptions<MyType> = {
+   *  keys: ['key1', 'key2']
+   * }
+   *
+   * const myFuse = new Fuse(list, options)
+   * let result = myFuse.search('pattern')
+   * ```
+   *
+   * @param pattern The pattern to search
+   * @param options `Fuse.FuseSearchOptions`
+   * @returns An array of search results
+   */
+  search<R = T>(
     pattern: string,
-    opts?: SearchOpt,
-  ): S extends true
-    ? M extends true
-      ? (Fuse.FuseResultWithMatches<R> & Fuse.FuseResultWithScore<R>)[]
-      : Fuse.FuseResultWithScore<R>[]
-    : M extends true
-    ? Fuse.FuseResultWithMatches<R>[]
-    : R[]
+    options?: Fuse.FuseSearchOptions,
+  ): Fuse.FuseResult<R>[]
 
   setCollection(
     list: ReadonlyArray<T>,
-    index?: ReadonlyArray<FuseIndexRecord>,
+    index?: ReadonlyArray<Fuse.FuseIndexRecord>,
   ): void
 
-  setIndex(index: ReadonlyArray<FuseIndexRecord>): void
+  setIndex(index: ReadonlyArray<Fuse.FuseIndexRecord>): void
+
+  /**
+   * Return the current version
+   */
+  static version: string
+
+  /**
+   * Use this method to pre-generate the index from the list, and pass it
+   * directly into the Fuse instance.
+   *
+   * _Note that Fuse will automatically index the table if one isn't provided
+   * during instantiation._
+   *
+   * ```typescript
+   * const list: MyType[] = [myType1, myType2, etc...]
+   *
+   * const index = Fuse.createIndex<MyType>(
+   *  keys: ['key1', 'key2']
+   *  list: list
+   * )
+   *
+   * const options: Fuse.IFuseOptions<MyType> = {
+   *  keys: ['key1', 'key2']
+   * }
+   *
+   * const myFuse = new Fuse(list, options, index)
+   * ```
+   * @param keys    The keys to index
+   * @param list    The list from which to create an index
+   * @param options?
+   * @returns An indexed list
+   */
+  static createIndex<U>(
+    keys: Fuse.FuseOptionKeyObject[] | string[],
+    list: ReadonlyArray<U>,
+    options?: Fuse.FuseIndexOptions<U>,
+  ): ReadonlyArray<Fuse.FuseIndexRecord>
 }
 
 declare namespace Fuse {
-  export interface FuseResultMatch {
+  type FuseGetFunction<T> = (
+    obj: T,
+    path: string,
+  ) => ReadonlyArray<string> | string
+
+  export type FuseIndexOptions<T> = {
+    getFn: FuseGetFunction<T>
+    ngrams: boolean
+  }
+
+  // {
+  //   title: { '$': "Old Man's War" },
+  //   'author.firstName': { '$': 'Codenar' }
+  // }
+  //
+  // OR
+  //
+  // {
+  //   tags: [
+  //     { $: 'nonfiction', idx: 1 },
+  //     { $: 'web development', idx: 0 },
+  //   ]
+  // }
+  export type FuseSortFunctionItem = {
+    [key: string]: { $: string } | { $: string; idx: number }[]
+  }
+
+  // {
+  //   score: 0.001,
+  //   key: 'author.firstName',
+  //   value: 'Codenar',
+  //   indices: [ [ 0, 3 ] ]
+  // }
+  export type FuseSortFunctionMatch = {
+    score: number
+    key: string
+    value: string
     indices: ReadonlyArray<number>[]
-    key?: string
-    refIndex?: number
-    value?: string
   }
-  export interface FuseResultWithScore<T> {
-    item: T
-    refIndex: number
+
+  // {
+  //   score: 0,
+  //   key: 'tags',
+  //   value: 'nonfiction',
+  //   idx: 1,
+  //   indices: [ [ 0, 9 ] ]
+  // }
+  export type FuseSortFunctionMatchList = FuseSortFunctionMatch & {
+    idx: number
+  }
+
+  export type FuseSortFunctionArg = {
+    idx: number
+    item: FuseSortFunctionItem
     score: number
+    matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]
   }
-  export interface FuseResultWithMatches<T> {
-    item: T
-    refIndex: number
-    matches: ReadonlyArray<FuseResultMatch>
+
+  export type FuseSortFunction = (
+    a: FuseSortFunctionArg,
+    b: FuseSortFunctionArg,
+  ) => number
+
+  // title: {
+  //   '$': "Old Man's War",
+  //   ng: [
+  //     ' ma', ' ol', ' wa',
+  //     "'s ", "an'", 'ar ',
+  //     'd m', 'ld ', 'man',
+  //     "n's", 'old', 's w',
+  //     'war'
+  //   ]
+  // }
+  type RecordEntryObject = { $: string; ng?: ReadonlyArray<string> }
+
+  // 'author.tags.name': [{
+  //   '$': 'pizza lover',
+  //   idx: 2,
+  //   ng: [
+  //     ' lo', ' pi', 'a l',
+  //     'er ', 'izz', 'lov',
+  //     'ove', 'piz', 'ver',
+  //     'za ', 'zza'
+  //   ]
+  // }
+  type RecordEntryArrayItem = ReadonlyArray<RecordEntryObject & { idx: number }>
+
+  // TODO: this makes it difficult to infer the type. Need to think more about this
+  type RecordEntry = { [key: string]: RecordEntryObject | RecordEntryArrayItem }
+
+  type FuseIndexRecord = {
+    idx: number
+    $: RecordEntry
+  }
+
+  // {
+  //   name: 'title',
+  //   weight: 0.7
+  // }
+  export type FuseOptionKeyObject = {
+    name: string
+    weight: number
   }
-  export interface FuseOptions<T> {
+
+  export interface IFuseOptions<T> {
     caseSensitive?: boolean
     distance?: number
     findAllMatches?: boolean
-    getFn?: (obj: any, path: string) => any
+    getFn?: FuseGetFunction<T>
     includeMatches?: boolean
     includeScore?: boolean
-    keys?: (keyof T | string)[] | { name: keyof T | string; weight: number }[]
+    keys?: FuseOptionKeyObject[] | string[]
     location?: number
     minMatchCharLength?: number
     shouldSort?: boolean
-    sortFn?: (a: { score: number }, b: { score: number }) => number
+    sortFn?: FuseSortFunction
     threshold?: number
     useExtendedSearch?: boolean
   }
+
+  // Denotes the start/end indices of a match
+  //                 start    end
+  //                   ↓       ↓
+  type RangeTuple = [number, number]
+
+  export type FuseResultMatch = {
+    indices: ReadonlyArray<RangeTuple>[]
+    key?: string
+    refIndex?: number
+    value?: string
+  }
+
+  export type FuseSearchOptions = {
+    limit: number
+  }
+
+  export type FuseResult<T> = {
+    item: T
+    refIndex: number
+    score?: number
+    matches?: ReadonlyArray<FuseResultMatch>
+  }
 }

File diff suppressed because it is too large
+ 0 - 1988
dist/fuse.dev.js


+ 1242 - 0
dist/fuse.esm.js

@@ -0,0 +1,1242 @@
+/**
+ * Fuse.js v5.0.7-beta - Lightweight fuzzy-search (http://fusejs.io)
+ *
+ * Copyright (c) 2020 Kiro Risk (http://kiro.me)
+ * All Rights Reserved. Apache Software License 2.0
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+function bitapScore(pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) {
+  const accuracy = errors / pattern.length;
+  const proximity = Math.abs(expectedLocation - currentLocation);
+
+  if (!distance) {
+    // Dodge divide by zero error.
+    return proximity ? 1.0 : accuracy
+  }
+
+  return accuracy + (proximity / distance)
+}
+
+function matchedIndiced(matchmask = [], minMatchCharLength = 1) {
+  let matchedIndices = [];
+  let start = -1;
+  let end = -1;
+  let i = 0;
+
+  for (let len = matchmask.length; i < len; i += 1) {
+    let match = matchmask[i];
+    if (match && start === -1) {
+      start = i;
+    } else if (!match && start !== -1) {
+      end = i - 1;
+      if ((end - start) + 1 >= minMatchCharLength) {
+        matchedIndices.push([start, end]);
+      }
+      start = -1;
+    }
+  }
+
+  // (i-1 - start) + 1 => i - start
+  if (matchmask[i - 1] && (i - start) >= minMatchCharLength) {
+    matchedIndices.push([start, i - 1]);
+  }
+
+  return matchedIndices
+}
+
+function bitapSearch(text, pattern, patternAlphabet, { location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1, includeMatches = false }) {
+  const patternLen = pattern.length;
+  // Set starting location at beginning text and initialize the alphabet.
+  const textLen = text.length;
+  // Handle the case when location > text.length
+  const expectedLocation = Math.max(0, Math.min(location, textLen));
+  // Highest score beyond which we give up.
+  let currentThreshold = threshold;
+  // Is there a nearby exact match? (speedup)
+  let bestLocation = text.indexOf(pattern, expectedLocation);
+
+  // a mask of the matches
+  const matchMask = [];
+  for (let i = 0; i < textLen; i += 1) {
+    matchMask[i] = 0;
+  }
+
+  if (bestLocation !== -1) {
+    let score = bitapScore(pattern, {
+      errors: 0,
+      currentLocation: bestLocation,
+      expectedLocation,
+      distance
+    });
+    currentThreshold = Math.min(score, currentThreshold);
+
+    // What about in the other direction? (speed up)
+    bestLocation = text.lastIndexOf(pattern, expectedLocation + patternLen);
+
+    if (bestLocation !== -1) {
+      let score = bitapScore(pattern, {
+        errors: 0,
+        currentLocation: bestLocation,
+        expectedLocation,
+        distance
+      });
+      currentThreshold = Math.min(score, currentThreshold);
+    }
+  }
+
+  // Reset the best location
+  bestLocation = -1;
+
+  let lastBitArr = [];
+  let finalScore = 1;
+  let binMax = patternLen + textLen;
+
+  const mask = 1 << (patternLen <= 31 ? patternLen - 1 : 30);
+
+  for (let i = 0; i < patternLen; i += 1) {
+    // Scan for the best match; each iteration allows for one more error.
+    // Run a binary search to determine how far from the match location we can stray
+    // at this error level.
+    let binMin = 0;
+    let binMid = binMax;
+
+    while (binMin < binMid) {
+      const score = bitapScore(pattern, {
+        errors: i,
+        currentLocation: expectedLocation + binMid,
+        expectedLocation,
+        distance
+      });
+
+      if (score <= currentThreshold) {
+        binMin = binMid;
+      } else {
+        binMax = binMid;
+      }
+
+      binMid = Math.floor((binMax - binMin) / 2 + binMin);
+    }
+
+    // Use the result from this iteration as the maximum for the next.
+    binMax = binMid;
+
+    let start = Math.max(1, expectedLocation - binMid + 1);
+    let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen;
+
+    // Initialize the bit array
+    let bitArr = Array(finish + 2);
+
+    bitArr[finish + 1] = (1 << i) - 1;
+
+    for (let j = finish; j >= start; j -= 1) {
+      let currentLocation = j - 1;
+      let charMatch = patternAlphabet[text.charAt(currentLocation)];
+
+      if (charMatch) {
+        matchMask[currentLocation] = 1;
+      }
+
+      // First pass: exact match
+      bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch;
+
+      // Subsequent passes: fuzzy match
+      if (i !== 0) {
+        bitArr[j] |= (((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1) | lastBitArr[j + 1];
+      }
+
+      if (bitArr[j] & mask) {
+        finalScore = bitapScore(pattern, {
+          errors: i,
+          currentLocation,
+          expectedLocation,
+          distance
+        });
+
+        // This match will almost certainly be better than any existing match.
+        // But check anyway.
+        if (finalScore <= currentThreshold) {
+          // Indeed it is
+          currentThreshold = finalScore;
+          bestLocation = currentLocation;
+
+          // Already passed `loc`, downhill from here on in.
+          if (bestLocation <= expectedLocation) {
+            break
+          }
+
+          // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
+          start = Math.max(1, 2 * expectedLocation - bestLocation);
+        }
+      }
+    }
+
+    // No hope for a (better) match at greater error levels.
+    const score = bitapScore(pattern, {
+      errors: i + 1,
+      currentLocation: expectedLocation,
+      expectedLocation,
+      distance
+    });
+
+    if (score > currentThreshold) {
+      break
+    }
+
+    lastBitArr = bitArr;
+  }
+
+  let result = {
+    isMatch: bestLocation >= 0,
+    // Count exact matches (those with a score of 0) to be "almost" exact
+    score: !finalScore ? 0.001 : finalScore,
+  };
+
+  if (includeMatches) {
+    result.matchedIndices = matchedIndiced(matchMask, minMatchCharLength);
+  }
+
+  return result
+}
+
+function patternAlphabet(pattern) {
+  let mask = {};
+  let len = pattern.length;
+
+  for (let i = 0; i < len; i += 1) {
+    mask[pattern.charAt(i)] = 0;
+  }
+
+  for (let i = 0; i < len; i += 1) {
+    mask[pattern.charAt(i)] |= 1 << (len - i - 1);
+  }
+
+  return mask
+}
+
+// Machine word size
+const MAX_BITS = 32;
+
+class BitapSearch {
+  constructor(pattern, {
+    // Approximately where in the text is the pattern expected to be found?
+    location = 0,
+    // Determines how close the match must be to the fuzzy location (specified above).
+    // An exact letter match which is 'distance' characters away from the fuzzy location
+    // would score as a complete mismatch. A distance of '0' requires the match be at
+    // the exact location specified, a threshold of '1000' would require a perfect match
+    // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
+    distance = 100,
+    // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
+    // (of both letters and location), a threshold of '1.0' would match anything.
+    threshold = 0.6,
+    // Indicates whether comparisons should be case sensitive.
+    isCaseSensitive = false,
+    // When true, the algorithm continues searching to the end of the input even if a perfect
+    // match is found before the end of the same input.
+    findAllMatches = false,
+    // Minimum number of characters that must be matched before a result is considered a match
+    minMatchCharLength = 1,
+
+    includeMatches = false
+  }) {
+    this.options = {
+      location,
+      distance,
+      threshold,
+      isCaseSensitive,
+      findAllMatches,
+      includeMatches,
+      minMatchCharLength
+    };
+
+    if (pattern.length > MAX_BITS) {
+      throw new Error(`Pattern length exceeds max of ${MAX_BITS}.`);
+    }
+
+    this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
+    this.patternAlphabet = patternAlphabet(this.pattern);
+  }
+
+  searchIn(value) {
+    let text = value.$;
+    return this.searchInString(text)
+  }
+
+  searchInString(text) {
+    const { isCaseSensitive, includeMatches } = this.options;
+
+    if (!isCaseSensitive) {
+      text = text.toLowerCase();
+    }
+
+    // Exact match
+    if (this.pattern === text) {
+      let result = {
+        isMatch: true,
+        score: 0
+      };
+
+      if (includeMatches) {
+        result.matchedIndices = [[0, text.length - 1]];
+      }
+
+      return result
+    }
+
+    // Otherwise, use Bitap algorithm
+    const { location, distance, threshold, findAllMatches, minMatchCharLength } = this.options;
+    return bitapSearch(text, this.pattern, this.patternAlphabet, {
+      location,
+      distance,
+      threshold,
+      findAllMatches,
+      minMatchCharLength,
+      includeMatches
+    })
+  }
+}
+
+// Token: 'file
+// Match type: exact-match
+// Description: Items that include `file`
+
+const isForPattern = pattern => pattern.charAt(0) == "'";
+
+const sanitize = pattern => pattern.substr(1);
+
+const match = (pattern, text) => {
+  const sanitizedPattern = sanitize(pattern);
+  const index = text.indexOf(sanitizedPattern);
+  const isMatch = index > -1;
+
+  return {
+    isMatch,
+    score: 0,
+  }
+};
+
+var exactMatch = {
+  isForPattern,
+  sanitize,
+  match
+};
+
+// Token: !fire
+// Match type: inverse-exact-match
+// Description: Items that do not include `fire`
+
+const isForPattern$1 = pattern => pattern.charAt(0) == '!';
+
+const sanitize$1 = pattern => pattern.substr(1);
+
+const match$1 = (pattern, text) => {
+  const sanitizedPattern = sanitize$1(pattern);
+  const isMatch = text.indexOf(sanitizedPattern) === -1;
+
+  return {
+    isMatch,
+    score: 0
+  }
+};
+
+var inverseExactMatch = {
+  isForPattern: isForPattern$1,
+  sanitize: sanitize$1,
+  match: match$1
+};
+
+// Token: ^file
+// Match type: prefix-exact-match
+// Description: Items that start with `file`
+
+const isForPattern$2 = pattern => pattern.charAt(0) == '^';
+
+const sanitize$2 = pattern => pattern.substr(1);
+
+const match$2 = (pattern, text) => {
+  const sanitizedPattern = sanitize$2(pattern);
+  const isMatch = text.startsWith(sanitizedPattern);
+
+  return {
+    isMatch,
+    score: 0
+  }
+};
+
+var prefixExactMatch = {
+  isForPattern: isForPattern$2,
+  sanitize: sanitize$2,
+  match: match$2
+};
+
+// Token: !^fire
+// Match type: inverse-prefix-exact-match
+// Description: Items that do not start with `fire`
+
+const isForPattern$3 = pattern => pattern.charAt(0) == '!' && pattern.charAt(1) == '^';
+
+const sanitize$3 = pattern => pattern.substr(2);
+
+const match$3 = (pattern, text) => {
+  const sanitizedPattern = sanitize$3(pattern);
+  const isMatch = !text.startsWith(sanitizedPattern);
+
+  return {
+    isMatch,
+    score: 0
+  }
+};
+
+var inversePrefixExactMatch = {
+  isForPattern: isForPattern$3,
+  sanitize: sanitize$3,
+  match: match$3
+};
+
+// Token: .file$
+// Match type: suffix-exact-match
+// Description: Items that end with `.file`
+
+const isForPattern$4 = pattern => pattern.charAt(pattern.length - 1) == '$';
+
+const sanitize$4 = pattern => pattern.substr(0, pattern.length - 1);
+
+const match$4 = (pattern, text) => {
+  const sanitizedPattern = sanitize$4(pattern);
+  const isMatch = text.endsWith(sanitizedPattern);
+
+  return {
+    isMatch,
+    score: 0
+  }
+};
+
+var suffixExactMatch = {
+  isForPattern: isForPattern$4,
+  sanitize: sanitize$4,
+  match: match$4
+};
+
+// Token: !.file$
+// Match type: inverse-suffix-exact-match
+// Description: Items that do not end with `.file`
+
+const isForPattern$5 = pattern => pattern.charAt(0) == '!' && pattern.charAt(pattern.length - 1) == '$';
+
+const sanitize$5 = pattern => pattern.substring(1, pattern.length - 1);
+
+const match$5 = (pattern, text) => {
+  const sanitizedPattern = sanitize$5(pattern);
+  const isMatch = !text.endsWith(sanitizedPattern);
+
+  return {
+    isMatch,
+    score: 0
+  }
+};
+
+var inverseSuffixExactMatch = {
+  isForPattern: isForPattern$5,
+  sanitize: sanitize$5,
+  match: match$5
+};
+
+const INFINITY = 1 / 0;
+
+const isArray = value => !Array.isArray
+  ? Object.prototype.toString.call(value) === '[object Array]'
+  : Array.isArray(value);
+
+// Adapted from:
+// https://github.com/lodash/lodash/blob/f4ca396a796435422bd4fd41fadbd225edddf175/.internal/baseToString.js
+const baseToString = value => {
+  // Exit early for strings to avoid a performance hit in some environments.
+  if (typeof value == 'string') {
+    return value;
+  }
+  let result = (value + '');
+  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+};
+
+const toString = value => value == null ? '' : baseToString(value);
+
+const isString = value => typeof value === 'string';
+
+const isNumber = value => typeof value === 'number';
+
+const isDefined = value => value !== undefined && value !== null;
+
+// Return a 2D array representation of the query, for simpler parsing.
+// Example:
+// "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]]
+const queryfy = (pattern) => pattern.split('|').map(item => item.trim().split(/ +/g));
+
+/**
+ * Command-like searching
+ * ======================
+ *
+ * Given multiple search terms delimited by spaces.e.g. `^jscript .python$ ruby !java`,
+ * search in a given text.
+ *
+ * Search syntax:
+ *
+ * | Token       | Match type                 | Description                            |
+ * | ----------- | -------------------------- | -------------------------------------- |
+ * | `jscript`   | fuzzy-match                | Items that match `jscript`             |
+ * | `'python`   | exact-match                | Items that include `python`            |
+ * | `!ruby`     | inverse-exact-match        | Items that do not include `ruby`       |
+ * | `^java`     | prefix-exact-match         | Items that start with `java`           |
+ * | `!^earlang` | inverse-prefix-exact-match | Items that do not start with `earlang` |
+ * | `.js$`      | suffix-exact-match         | Items that end with `.js`              |
+ * | `!.go$`     | inverse-suffix-exact-match | Items that do not end with `.go`       |
+ *
+ * A single pipe character acts as an OR operator. For example, the following
+ * query matches entries that start with `core` and end with either`go`, `rb`,
+ * or`py`.
+ *
+ * ```
+ * ^core go$ | rb$ | py$
+ * ```
+ */
+class ExtendedSearch {
+  constructor(pattern, options) {
+    const { isCaseSensitive } = options;
+    this.query = null;
+    this.options = options;
+    // A <pattern>:<BitapSearch> key-value pair for optimizing searching
+    this._fuzzyCache = {};
+
+    if (isString(pattern) && pattern.trim().length > 0) {
+      this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
+      this.query = queryfy(this.pattern);
+    }
+  }
+
+  searchIn(value) {
+    const query = this.query;
+
+    if (!this.query) {
+      return {
+        isMatch: false,
+        score: 1
+      }
+    }
+
+    let text = value.$;
+
+    text = this.options.isCaseSensitive ? text : text.toLowerCase();
+
+    let matchFound = false;
+
+    for (let i = 0, qLen = query.length; i < qLen; i += 1) {
+
+      const parts = query[i];
+      let result = null;
+      matchFound = true;
+
+      for (let j = 0, pLen = parts.length; j < pLen; j += 1) {
+        let token = parts[j];
+        result = this._search(token, text);
+        if (!result.isMatch) {
+          // AND condition, short-circuit and move on to next part
+          matchFound = false;
+          break
+        }
+      }
+
+      // OR condition, so if TRUE, return
+      if (matchFound) {
+        return result
+      }
+    }
+
+    // Nothing was matched
+    return {
+      isMatch: false,
+      score: 1
+    }
+  }
+
+  _search(pattern, text) {
+    if (exactMatch.isForPattern(pattern)) {
+      return exactMatch.match(pattern, text)
+    } else if (prefixExactMatch.isForPattern(pattern)) {
+      return prefixExactMatch.match(pattern, text)
+    } else if (inversePrefixExactMatch.isForPattern(pattern)) {
+      return inversePrefixExactMatch.match(pattern, text)
+    } else if (inverseSuffixExactMatch.isForPattern(pattern)) {
+      return inverseSuffixExactMatch.match(pattern, text)
+    } else if (suffixExactMatch.isForPattern(pattern)) {
+      return suffixExactMatch.match(pattern, text)
+    } else if (inverseExactMatch.isForPattern(pattern)) {
+      return inverseExactMatch.match(pattern, text)
+    } else {
+      let searcher = this._fuzzyCache[pattern];
+      if (!searcher) {
+        searcher = new BitapSearch(pattern, this.options);
+        this._fuzzyCache[pattern] = searcher;
+      }
+      return searcher.searchInString(text)
+    }
+  }
+}
+
+const NGRAM_LEN = 3;
+
+function ngram(text, { n = NGRAM_LEN, pad = true, sort = false }) {
+  let nGrams = [];
+
+  if (text === null || text === undefined) {
+    return nGrams
+  }
+
+  text = text.toLowerCase();
+  if (pad) {
+    text = ` ${text} `;
+  }
+
+  let index = text.length - n + 1;
+  if (index < 1) {
+    return nGrams
+  }
+
+  while (index--) {
+    nGrams[index] = text.substr(index, n);
+  }
+
+  if (sort) {
+    nGrams.sort((a, b) => a == b ? 0 : a < b ? -1 : 1);
+  }
+
+  return nGrams
+}
+
+// Assumes arrays are sorted
+function union (arr1, arr2) {
+  let result = [];
+  let i = 0;
+  let j = 0;
+
+  while (i < arr1.length && j < arr2.length) {
+    let item1 = arr1[i];
+    let item2 = arr2[j];
+
+    if (item1 < item2) {
+      result.push(item1);
+      i += 1;
+    } else if (item2 < item1) {
+      result.push(item2);
+      j += 1;
+    } else {
+      result.push(item2);
+      i += 1;
+      j += 1;
+    }
+  }
+
+  while (i < arr1.length) {
+    result.push(arr1[i]);
+    i += 1;
+  }
+
+  while (j < arr2.length) {
+    result.push(arr2[j]);
+    j += 1;
+  }
+
+  return result;
+}
+
+// Assumes arrays are sorted
+function intersection(arr1, arr2) {
+  let result = [];
+  let i = 0;
+  let j = 0;
+
+  while (i < arr1.length && j < arr2.length) {
+    let item1 = arr1[i];
+    let item2 = arr2[j];
+
+    if (item1 == item2) {
+      result.push(item1);
+      i += 1;
+      j += 1;
+    } else if (item1 < item2) {
+      i += 1;
+    } else if (item1 > item2) {
+      j += 1;
+    } else {
+      i += 1;
+      j += 1;
+    }
+  }
+
+  return result;
+}
+
+function jaccardDistance(nGram1, nGram2) {
+  let nGramUnion = union(nGram1, nGram2);
+  let nGramIntersection = intersection(nGram1, nGram2);
+
+  return 1 - nGramIntersection.length / nGramUnion.length
+}
+
+class NGramSearch {
+  constructor(pattern, options = { threshold: 0.6 }) {
+    // Create the ngram, and sort it
+    this.options = options;
+    this.patternNgram = ngram(pattern, { sort: true });
+  }
+  searchIn(value) {
+    let textNgram = value.ng;
+    if (!textNgram) {
+      textNgram = ngram(value.$, { sort: true });
+      value.ng = textNgram;
+    }
+
+    let jacardResult = jaccardDistance(this.patternNgram, textNgram);
+
+    const isMatch = jacardResult < this.options.threshold;
+
+    return {
+      score: isMatch ? jacardResult : 1,
+      isMatch
+    }
+  }
+}
+
+function get(obj, path) {
+  let list = [];
+  let arr = false;
+
+  const _get = (obj, path) => {
+    if (!path) {
+      // If there's no path left, we've gotten to the object we care about.
+      list.push(obj);
+    } else {
+      const dotIndex = path.indexOf('.');
+
+      let key = path;
+      let remaining = null;
+
+      if (dotIndex !== -1) {
+        key = path.slice(0, dotIndex);
+        remaining = path.slice(dotIndex + 1);
+      }
+
+      const value = obj[key];
+
+      if (isDefined(value)) {
+        if (!remaining && (isString(value) || isNumber(value))) {
+          list.push(toString(value));
+        } else if (isArray(value)) {
+          arr = true;
+          // Search each item in the array.
+          for (let i = 0, len = value.length; i < len; i += 1) {
+            _get(value[i], remaining);
+          }
+        } else if (remaining) {
+          // An object. Recurse further.
+          _get(value, remaining);
+        }
+      }
+    }
+  };
+
+  _get(obj, path);
+
+  if (arr) {
+    return list
+  }
+
+  return list[0]
+}
+
+function createIndex(keys, list, { getFn = get, ngrams = false } = {}) {
+  let indexedList = [];
+
+  // List is Array<String>
+  if (isString(list[0])) {
+    // Iterate over every string in the list
+    for (let i = 0, len = list.length; i < len; i += 1) {
+      const value = list[i];
+
+      if (isDefined(value)) {
+        // if (!isCaseSensitive) {
+        //   value = value.toLowerCase()
+        // }
+
+        let record = {
+          $: value,
+          idx: i
+        };
+
+        if (ngrams) {
+          record.ng = ngram(value, { sort: true });
+        }
+
+        indexedList.push(record);
+      }
+    }
+
+  } else {
+    // List is Array<Object>
+    const keysLen = keys.length;
+
+    for (let i = 0, len = list.length; i < len; i += 1) {
+      let item = list[i];
+
+      let record = { idx: i, $: {} };
+
+      // Iterate over every key (i.e, path), and fetch the value at that key
+      for (let j = 0; j < keysLen; j += 1) {
+        let key = keys[j];
+        let value = getFn(item, key);
+
+        if (!isDefined(value)) {
+          continue
+        }
+
+        if (isArray(value)) {
+          let subRecords = [];
+          const stack = [{ arrayIndex: -1, value }];
+
+          while (stack.length) {
+            const { arrayIndex, value } = stack.pop();
+
+            if (!isDefined(value)) {
+              continue
+            }
+
+            if (isString(value)) {
+
+              // if (!isCaseSensitive) {
+              //   v = v.toLowerCase()
+              // }
+
+              let subRecord = { $: value, idx: arrayIndex };
+
+              if (ngrams) {
+                subRecord.ng = ngram(value, { sort: true });
+              }
+
+              subRecords.push(subRecord);
+
+            } else if (isArray(value)) {
+              for (let k = 0, arrLen = value.length; k < arrLen; k += 1) {
+                stack.push({
+                  arrayIndex: k,
+                  value: value[k],
+                });
+              }
+            }
+          }
+          record.$[key] = subRecords;
+        } else {
+          // if (!isCaseSensitive) {
+          //   value = value.toLowerCase()
+          // }
+
+          let subRecord = { $: value };
+
+          if (ngrams) {
+            subRecord.ng = ngram(value, { sort: true });
+          }
+
+          record.$[key] = subRecord;
+        }
+      }
+
+      indexedList.push(record);
+    }
+  }
+
+  return indexedList
+}
+
+class KeyStore {
+  constructor(keys) {
+    this._keys = {};
+    this._keyNames = [];
+    this._length = keys.length;
+
+    // Iterate over every key
+    if (keys.length && isString(keys[0])) {
+      for (let i = 0; i < this._length; i += 1) {
+        const key = keys[i];
+        this._keys[key] = {
+          weight: 1
+        };
+        this._keyNames.push(key);
+      }
+    } else {
+      let totalWeight = 0;
+
+      for (let i = 0; i < this._length; i += 1) {
+        const key = keys[i];
+
+        if (!key.hasOwnProperty('name')) {
+          throw new Error('Missing "name" property in key object')
+        }
+
+        const keyName = key.name;
+        this._keyNames.push(keyName);
+
+        if (!key.hasOwnProperty('weight')) {
+          throw new Error('Missing "weight" property in key object')
+        }
+
+        const weight = key.weight;
+
+        if (weight <= 0 || weight >= 1) {
+          throw new Error('"weight" property in key must bein the range of (0, 1)')
+        }
+
+        this._keys[keyName] = {
+          weight
+        };
+
+        totalWeight += weight;
+      }
+
+      // Normalize weights so that their sum is equal to 1
+      for (let i = 0; i < this._length; i += 1) {
+        const keyName = this._keyNames[i];
+        const keyWeight = this._keys[keyName].weight;
+        this._keys[keyName].weight = keyWeight / totalWeight;
+      }
+    }
+  }
+  get(key, name) {
+    return this._keys[key] ? this._keys[key][name] : -1
+  }
+  keys() {
+    return this._keyNames
+  }
+  count() {
+    return this._length
+  }
+  toJSON() {
+    return JSON.stringify(this._keys)
+  }
+}
+
+function transformMatches(result, data) {
+  const matches = result.matches;
+  data.matches = [];
+
+  if (!isDefined(matches)) {
+    return
+  }
+
+  for (let i = 0, len = matches.length; i < len; i += 1) {
+    let match = matches[i];
+
+    if (!isDefined(match.indices) || match.indices.length === 0) {
+      continue
+    }
+
+    let obj = {
+      indices: match.indices,
+      value: match.value
+    };
+
+    if (match.key) {
+      obj.key = match.key;
+    }
+
+    if (match.idx > -1) {
+      obj.refIndex = match.idx;
+    }
+
+    data.matches.push(obj);
+  }
+}
+
+function transformScore(result, data) {
+  data.score = result.score;
+}
+
+const FuseOptions = {
+  // When true, the algorithm continues searching to the end of the input even if a perfect
+  // match is found before the end of the same input.
+  isCaseSensitive: false,
+  // Determines how close the match must be to the fuzzy location (specified above).
+  // An exact letter match which is 'distance' characters away from the fuzzy location
+  // would score as a complete mismatch. A distance of '0' requires the match be at
+  // the exact location specified, a threshold of '1000' would require a perfect match
+  // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
+  distance: 100,
+  // Minimum number of characters that must be matched before a result is considered a match
+  findAllMatches: false,
+  // The get function to use when fetching an object's properties.
+  // The default will search nested paths *ie foo.bar.baz*
+  getFn: get,
+  includeMatches: false,
+  includeScore: false,
+  // List of properties that will be searched. This also supports nested properties.
+  keys: [],
+  // Approximately where in the text is the pattern expected to be found?
+  location: 0,
+  // Minimum number of characters that must be matched before a result is considered a match
+  minMatchCharLength: 1,
+  // Whether to sort the result list, by score
+  shouldSort: true,
+  // Default sort function
+  sortFn: (a, b) => (a.score - b.score),
+  // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
+  // (of both letters and location), a threshold of '1.0' would match anything.
+  threshold: 0.6,
+  // Enabled extended-searching
+  useExtendedSearch: false
+};
+
+class Fuse {
+  constructor(list, options = FuseOptions, index = null) {
+    this.options = { ...FuseOptions, ...options };
+    // `caseSensitive` is deprecated, use `isCaseSensitive` instead
+    this.options.isCaseSensitive = options.caseSensitive;
+    delete this.options.caseSensitive;
+
+    this._processKeys(this.options.keys);
+    this.setCollection(list, index);
+  }
+
+  setCollection(list, index = null) {
+    this.list = list;
+    this.listIsStringArray = isString(list[0]);
+
+    if (index) {
+      this.setIndex(index);
+    } else {
+      this.setIndex(this._createIndex());
+    }
+  }
+
+  setIndex(listIndex) {
+    this._indexedList = listIndex;
+  }
+
+  _processKeys(keys) {
+    this._keyStore = new KeyStore(keys);
+  }
+
+  _createIndex() {
+    return createIndex(this._keyStore.keys(), this.list, {
+      getFn: this.options.getFn
+    })
+  }
+
+  search(pattern, opts = { limit: false }) {
+    const { useExtendedSearch, shouldSort } = this.options;
+
+    let searcher = null;
+
+    if (useExtendedSearch) {
+      searcher = new ExtendedSearch(pattern, this.options);
+    } else if (pattern.length > MAX_BITS) {
+      searcher = new NGramSearch(pattern, this.options);
+    } else {
+      searcher = new BitapSearch(pattern, this.options);
+    }
+
+    let results = this._searchUsing(searcher);
+
+    this._computeScore(results);
+
+    if (shouldSort) {
+      this._sort(results);
+    }
+
+    if (opts.limit && isNumber(opts.limit)) {
+      results = results.slice(0, opts.limit);
+    }
+
+    return this._format(results)
+  }
+
+  _searchUsing(searcher) {
+    const list = this._indexedList;
+    const results = [];
+    const { includeMatches } = this.options;
+
+    // List is Array<String>
+    if (this.listIsStringArray) {
+      // Iterate over every string in the list
+      for (let i = 0, len = list.length; i < len; i += 1) {
+        let value = list[i];
+        let { $: text, idx } = value;
+
+        if (!isDefined(text)) {
+          continue
+        }
+
+        let searchResult = searcher.searchIn(value);
+
+        const { isMatch, score } = searchResult;
+
+        if (!isMatch) {
+          continue
+        }
+
+        let match = { score, value: text };
+
+        if (includeMatches) {
+          match.indices = searchResult.matchedIndices;
+        }
+
+        results.push({
+          item: text,
+          idx,
+          matches: [match]
+        });
+      }
+
+    } else {
+      // List is Array<Object>
+      const keyNames = this._keyStore.keys();
+      const keysLen = this._keyStore.count();
+
+      for (let i = 0, len = list.length; i < len; i += 1) {
+        let { $: item, idx } = list[i];
+
+        if (!isDefined(item)) {
+          continue
+        }
+
+        let matches = [];
+
+        // Iterate over every key (i.e, path), and fetch the value at that key
+        for (let j = 0; j < keysLen; j += 1) {
+          let key = keyNames[j];
+          let value = item[key];
+
+          if (!isDefined(value)) {
+            continue
+          }
+
+          if (isArray(value)) {
+            for (let k = 0, len = value.length; k < len; k += 1) {
+              let arrItem = value[k];
+              let text = arrItem.$;
+              let idx = arrItem.idx;
+
+              if (!isDefined(text)) {
+                continue
+              }
+
+              let searchResult = searcher.searchIn(arrItem);
+
+              const { isMatch, score } = searchResult;
+
+              if (!isMatch) {
+                continue
+              }
+
+              let match = { score, key, value: text, idx };
+
+              if (includeMatches) {
+                match.indices = searchResult.matchedIndices;
+              }
+
+              matches.push(match);
+            }
+          } else {
+            let text = value.$;
+            let searchResult = searcher.searchIn(value);
+
+            const { isMatch, score } = searchResult;
+
+            if (!isMatch) {
+              continue
+            }
+
+            let match = { score, key, value: text };
+
+            if (includeMatches) {
+              match.indices = searchResult.matchedIndices;
+            }
+
+            matches.push(match);
+          }
+        }
+
+        if (matches.length) {
+          results.push({
+            idx,
+            item,
+            matches
+          });
+        }
+      }
+    }
+
+    return results
+  }
+
+  _computeScore(results) {
+    for (let i = 0, len = results.length; i < len; i += 1) {
+      const result = results[i];
+      const matches = result.matches;
+      const scoreLen = matches.length;
+
+      let totalWeightedScore = 1;
+
+      for (let j = 0; j < scoreLen; j += 1) {
+        const item = matches[j];
+        const key = item.key;
+        const keyWeight = this._keyStore.get(key, 'weight');
+        const weight = keyWeight > -1 ? keyWeight : 1;
+        const score = item.score === 0 && keyWeight > -1
+          ? Number.EPSILON
+          : item.score;
+
+        totalWeightedScore *= Math.pow(score, weight);
+      }
+
+      result.score = totalWeightedScore;
+    }
+  }
+
+  _sort(results) {
+    results.sort(this.options.sortFn);
+  }
+
+  _format(results) {
+    const finalOutput = [];
+
+    const { includeMatches, includeScore, } = this.options;
+
+    let transformers = [];
+
+    if (includeMatches) transformers.push(transformMatches);
+    if (includeScore) transformers.push(transformScore);
+
+    for (let i = 0, len = results.length; i < len; i += 1) {
+      const result = results[i];
+      const { idx } = result;
+
+      const data = {
+        item: this.list[idx],
+        refIndex: idx
+      };
+
+      if (transformers.length) {
+        for (let j = 0, len = transformers.length; j < len; j += 1) {
+          transformers[j](result, data);
+        }
+      }
+
+      finalOutput.push(data);
+    }
+
+    return finalOutput
+  }
+}
+
+Fuse.version = '5.0.7-beta';
+Fuse.createIndex = createIndex;
+
+export default Fuse;

File diff suppressed because it is too large
+ 4 - 4
dist/fuse.js


File diff suppressed because it is too large
+ 8 - 0
dist/fuse.min.js


File diff suppressed because it is too large
+ 0 - 0
dist/fuse.raw.js.map


+ 1 - 1
jest.config.js

@@ -1,4 +1,4 @@
 module.exports = {
   testEnvironment: 'node',
   testMatch: ['<rootDir>/test/*.test.(ts|js)']
-}
+}

+ 25 - 16
package.json

@@ -1,10 +1,18 @@
 {
   "name": "fuse.js",
   "author": {
-    "name": "Kirollos Risk",
+    "name": "Kiro Risk",
     "email": "kirollos@gmail.com",
     "url": "http://kiro.me"
   },
+  "main": "./dist/fuse.common.js",
+  "module": "./dist/fuse.esm.js",
+  "unpkg": "./dist/fuse.js",
+  "jsdelivr": "./dist/fuse.js",
+  "typings": "./dist/fuse.d.ts",
+  "files": [
+    "dist"
+  ],
   "version": "5.0.7-beta",
   "description": "Lightweight fuzzy-search",
   "license": "Apache-2.0",
@@ -18,37 +26,38 @@
     "search",
     "bitap"
   ],
-  "main": "dist/fuse.js",
-  "types": "dist/fuse.d.ts",
   "scripts": {
-    "clean": "rimraf ./dist",
+    "dev": "rollup -w -c scripts/configs.js --environment TARGET:umd-dev",
+    "dev:cjs": "rollup -w -c scripts/configs.js --environment TARGET:commonjs",
+    "dev:esm": "rollup -w -c scripts/configs.js --environment TARGET:esm",
+    "build": "node scripts/build.main.js",
     "test": "jest",
-    "prebuild": "npm run clean",
-    "build": "webpack --config ./configs/webpack.output.js",
-    "predev": "npm run clean",
-    "dev": "webpack --progress --colors --watch --config ./configs/webpack.dev.js"
+    "release": "bash scripts/release.sh"
   },
   "devDependencies": {
     "@babel/cli": "7.2.3",
     "@babel/core": "^7.3.4",
+    "@babel/plugin-proposal-object-rest-spread": "7.9.0",
     "@babel/preset-env": "7.3.4",
-    "@babel/preset-typescript": "^7.3.3",
-    "@types/jest": "^24.0.11",
+    "@babel/preset-typescript": "7.9.0",
+    "@rollup/plugin-buble": "0.21.1",
+    "@rollup/plugin-node-resolve": "7.1.1",
+    "@rollup/plugin-replace": "2.3.1",
+    "@types/jest": "25.1.4",
     "babel-loader": "^8.0.5",
-    "copy-webpack-plugin": "5.1.1",
     "faker": "4.1.0",
     "jest": "25.1.0",
     "rimraf": "3.0.2",
+    "rollup": "2.1.0",
+    "rollup-plugin-babel": "4.4.0",
+    "rollup-plugin-copy": "3.3.0",
     "terser-webpack-plugin": "2.3.5",
+    "typescript": "3.8.3",
     "webpack": "4.42.0",
-    "webpack-cli": "3.3.11",
-    "webpack-merge": "4.2.2"
+    "webpack-cli": "3.3.11"
   },
   "engines": {
     "node": ">=10"
   },
-  "files": [
-    "dist/"
-  ],
   "dependencies": {}
 }

+ 83 - 0
scripts/build.main.js

@@ -0,0 +1,83 @@
+const fs = require('fs')
+const path = require('path')
+const zlib = require('zlib')
+const terser = require('terser')
+const rollup = require('rollup')
+const configs = require('./configs')
+
+if (!fs.existsSync('dist')) {
+  fs.mkdirSync('dist')
+}
+
+build(Object.keys(configs).map(key => configs[key]))
+
+async function build(builds) {
+  for (const entry of builds) {
+    try {
+      await buildEntry(entry)
+    } catch (err) {
+      logError(err)
+    }
+  }
+}
+
+async function buildEntry(config) {
+  const output = config.output
+  const { file, banner } = output
+  const isProd = /(min|prod)\.js$/.test(file)
+
+  try {
+    let bundle = await rollup.rollup(config)
+    let { output: [{ code }] } = await bundle.generate(output)
+
+    if (isProd) {
+      const minified = (banner || '') + terser.minify(code, {
+        toplevel: true,
+        output: {
+          ascii_only: true
+        },
+        compress: {
+          pure_funcs: ['makeMap']
+        }
+      }).code
+      return write(file, minified, true)
+    } else {
+      return write(file, code)
+    }
+  } catch (err) {
+    throw new Error(err)
+  }
+}
+
+function write(dest, code, zip) {
+  return new Promise((resolve, reject) => {
+    function report(extra) {
+      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
+      resolve()
+    }
+
+    fs.writeFile(dest, code, err => {
+      if (err) return reject(err)
+      if (zip) {
+        zlib.gzip(code, (err, zipped) => {
+          if (err) return reject(err)
+          report(` (gzipped: ${getSize(zipped)})`)
+        })
+      } else {
+        report()
+      }
+    })
+  })
+}
+
+function getSize(code) {
+  return (code.length / 1024).toFixed(2) + 'kb'
+}
+
+function logError(e) {
+  console.error(e)
+}
+
+function blue(str) {
+  return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m`
+}

+ 120 - 0
scripts/configs.js

@@ -0,0 +1,120 @@
+const path = require('path')
+const buble = require('@rollup/plugin-buble')
+const replace = require('@rollup/plugin-replace')
+const node = require('@rollup/plugin-node-resolve')
+const babel = require('rollup-plugin-babel')
+const copy = require('rollup-plugin-copy')
+const featureFlags = require('./feature-flags')
+const pckg = require('../package.json')
+
+const FILENAME = 'fuse'
+const VERSION = process.env.VERSION || pckg.version
+const AUTHOR = pckg.author
+const HOMEPAGE = pckg.homepage
+const DESCRIPTION = pckg.description
+
+const banner = `/**
+ * Fuse.js v${VERSION} - ${DESCRIPTION} (${HOMEPAGE})
+ *
+ * Copyright (c) ${new Date().getFullYear()} ${AUTHOR.name} (${AUTHOR.url})
+ * All Rights Reserved. Apache Software License 2.0
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */\n`
+
+const resolve = _path => path.resolve(__dirname, '../', _path)
+
+const builds = {
+  // UMD build
+  'umd-dev': {
+    entry: resolve('src/index.js'),
+    dest: resolve(`dist/${FILENAME}.js`),
+    format: 'umd',
+    env: 'development',
+    plugins: [copy({
+      targets: [{
+        src: resolve('src/index.d.ts'),
+        dest: resolve('dist'),
+        rename: `${FILENAME}.d.ts`,
+        transform: (content) => {
+          return `// Type definitions for Fuse.js v${VERSION}\n${content}`
+        }
+      }]
+    })]
+  },
+  // UMD production build
+  'umd-prod': {
+    entry: resolve('src/index.js'),
+    dest: resolve(`dist/${FILENAME}.min.js`),
+    format: 'umd',
+    env: 'production',
+  },
+  // CommonJS build
+  'commonjs': {
+    entry: resolve('src/index.js'),
+    dest: resolve(`dist/${FILENAME}.common.js`),
+    env: 'development',
+    format: 'cjs',
+  },
+  // ES modules build (for bundlers)
+  'esm': {
+    entry: resolve('src/index.js'),
+    dest: resolve(`dist/${FILENAME}.esm.js`),
+    format: 'es',
+    env: 'development',
+    transpile: false,
+  }
+}
+// built-in vars
+const vars = {
+  __VERSION__: VERSION
+}
+
+function genConfig(options) {
+  const config = {
+    input: options.entry,
+    plugins: [
+      node(),
+      ...options.plugins || [],
+    ],
+    output: {
+      banner,
+      file: options.dest,
+      format: options.format,
+      name: 'Fuse.js'
+    }
+  }
+
+  // build-specific env
+  if (options.env) {
+    vars['process.env.NODE_ENV'] = JSON.stringify(options.env)
+  }
+
+  // feature flags
+  Object.keys(featureFlags).forEach(key => {
+    vars[`process.env.${key}`] = featureFlags[key]
+  })
+
+  config.plugins.push(replace(vars))
+
+  if (options.transpile !== false) {
+    config.plugins.push(babel())
+    config.plugins.push(buble())
+  }
+
+  return config
+}
+
+function mapValues(obj, fn) {
+  const res = {}
+  Object.keys(obj).forEach(key => {
+    res[key] = fn(obj[key], key)
+  })
+  return res
+}
+
+if (process.env.TARGET) {
+  module.exports = genConfig(builds[process.env.TARGET])
+} else {
+  module.exports = mapValues(builds, genConfig)
+}

+ 3 - 0
scripts/feature-flags.js

@@ -0,0 +1,3 @@
+module.exports = {
+  EXTENDED_SEARCH: false
+}

+ 10 - 5
scripts/release.sh

@@ -1,16 +1,16 @@
 #!/usr/bin/env bash
 
-version='';
+VERSION='';
 re="\"(version)\": \"([^\"]*)\"";
 
 while read -r l; do
   if [[ $l =~ $re ]]; then
     value="${BASH_REMATCH[2]}";
-    version="$value";
+    VERSION="$value";
   fi
 done < package.json;
 
-echo $version;
+echo $VERSION;
 
 on_master_branch () {
   [[ $(git symbolic-ref --short -q HEAD) == "master" ]] && return 0
@@ -28,10 +28,15 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
   echo -e "\033[0;32mReleasing...\033[0m"
   echo
   yarn build
-  git commit -a -m "Build version $version"
-  git tag -a v$version -m "Version $version"
+  git commit -a -m "Build version $VERSION"
+
+  # tag version
+  git tag -a v$version -m "Version $VERSION"
   git push origin master
   git push --tags
+
+  # publish
+  npm publish
 else
   echo -e "\033[0;31mCancelling...\033[0m"
 fi

+ 284 - 0
src/core/index.js

@@ -0,0 +1,284 @@
+
+import { BitapSearch, ExtendedSearch, NGramSearch } from '../search'
+import { isArray, isDefined, isString, isNumber } from '../helpers/type-checkers'
+import get from '../helpers/get'
+import { createIndex, KeyStore } from '../tools'
+import { transformMatches, transformScore } from '../transform'
+import { MAX_BITS } from '../search/bitap-search/constants'
+
+const FuseOptions = {
+  // When true, the algorithm continues searching to the end of the input even if a perfect
+  // match is found before the end of the same input.
+  isCaseSensitive: false,
+  // Determines how close the match must be to the fuzzy location (specified above).
+  // An exact letter match which is 'distance' characters away from the fuzzy location
+  // would score as a complete mismatch. A distance of '0' requires the match be at
+  // the exact location specified, a threshold of '1000' would require a perfect match
+  // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
+  distance: 100,
+  // Minimum number of characters that must be matched before a result is considered a match
+  findAllMatches: false,
+  // The get function to use when fetching an object's properties.
+  // The default will search nested paths *ie foo.bar.baz*
+  getFn: get,
+  includeMatches: false,
+  includeScore: false,
+  // List of properties that will be searched. This also supports nested properties.
+  keys: [],
+  // Approximately where in the text is the pattern expected to be found?
+  location: 0,
+  // Minimum number of characters that must be matched before a result is considered a match
+  minMatchCharLength: 1,
+  // Whether to sort the result list, by score
+  shouldSort: true,
+  // Default sort function
+  sortFn: (a, b) => (a.score - b.score),
+  // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
+  // (of both letters and location), a threshold of '1.0' would match anything.
+  threshold: 0.6,
+  // Enabled extended-searching
+  useExtendedSearch: false
+}
+
+export default class Fuse {
+  constructor(list, options = FuseOptions, index = null) {
+    this.options = { ...FuseOptions, ...options }
+    // `caseSensitive` is deprecated, use `isCaseSensitive` instead
+    this.options.isCaseSensitive = options.caseSensitive
+    delete this.options.caseSensitive
+
+    this._processKeys(this.options.keys)
+    this.setCollection(list, index)
+  }
+
+  setCollection(list, index = null) {
+    this.list = list
+    this.listIsStringArray = isString(list[0])
+
+    if (index) {
+      this.setIndex(index)
+    } else {
+      this.setIndex(this._createIndex())
+    }
+  }
+
+  setIndex(listIndex) {
+    this._indexedList = listIndex
+  }
+
+  _processKeys(keys) {
+    this._keyStore = new KeyStore(keys)
+  }
+
+  _createIndex() {
+    return createIndex(this._keyStore.keys(), this.list, {
+      getFn: this.options.getFn
+    })
+  }
+
+  search(pattern, opts = { limit: false }) {
+    const { useExtendedSearch, shouldSort } = this.options
+
+    let searcher = null
+
+    if (useExtendedSearch) {
+      searcher = new ExtendedSearch(pattern, this.options)
+    } else if (pattern.length > MAX_BITS) {
+      searcher = new NGramSearch(pattern, this.options)
+    } else {
+      searcher = new BitapSearch(pattern, this.options)
+    }
+
+    let results = this._searchUsing(searcher)
+
+    this._computeScore(results)
+
+    if (shouldSort) {
+      this._sort(results)
+    }
+
+    if (opts.limit && isNumber(opts.limit)) {
+      results = results.slice(0, opts.limit)
+    }
+
+    return this._format(results)
+  }
+
+  _searchUsing(searcher) {
+    const list = this._indexedList
+    const results = []
+    const { includeMatches } = this.options
+
+    // List is Array<String>
+    if (this.listIsStringArray) {
+      // Iterate over every string in the list
+      for (let i = 0, len = list.length; i < len; i += 1) {
+        let value = list[i]
+        let { $: text, idx } = value
+
+        if (!isDefined(text)) {
+          continue
+        }
+
+        let searchResult = searcher.searchIn(value)
+
+        const { isMatch, score } = searchResult
+
+        if (!isMatch) {
+          continue
+        }
+
+        let match = { score, value: text }
+
+        if (includeMatches) {
+          match.indices = searchResult.matchedIndices
+        }
+
+        results.push({
+          item: text,
+          idx,
+          matches: [match]
+        })
+      }
+
+    } else {
+      // List is Array<Object>
+      const keyNames = this._keyStore.keys()
+      const keysLen = this._keyStore.count()
+
+      for (let i = 0, len = list.length; i < len; i += 1) {
+        let { $: item, idx } = list[i]
+
+        if (!isDefined(item)) {
+          continue
+        }
+
+        let matches = []
+
+        // Iterate over every key (i.e, path), and fetch the value at that key
+        for (let j = 0; j < keysLen; j += 1) {
+          let key = keyNames[j]
+          let value = item[key]
+
+          if (!isDefined(value)) {
+            continue
+          }
+
+          if (isArray(value)) {
+            for (let k = 0, len = value.length; k < len; k += 1) {
+              let arrItem = value[k]
+              let text = arrItem.$
+              let idx = arrItem.idx
+
+              if (!isDefined(text)) {
+                continue
+              }
+
+              let searchResult = searcher.searchIn(arrItem)
+
+              const { isMatch, score } = searchResult
+
+              if (!isMatch) {
+                continue
+              }
+
+              let match = { score, key, value: text, idx }
+
+              if (includeMatches) {
+                match.indices = searchResult.matchedIndices
+              }
+
+              matches.push(match)
+            }
+          } else {
+            let text = value.$
+            let searchResult = searcher.searchIn(value)
+
+            const { isMatch, score } = searchResult
+
+            if (!isMatch) {
+              continue
+            }
+
+            let match = { score, key, value: text }
+
+            if (includeMatches) {
+              match.indices = searchResult.matchedIndices
+            }
+
+            matches.push(match)
+          }
+        }
+
+        if (matches.length) {
+          results.push({
+            idx,
+            item,
+            matches
+          })
+        }
+      }
+    }
+
+    return results
+  }
+
+  _computeScore(results) {
+    for (let i = 0, len = results.length; i < len; i += 1) {
+      const result = results[i]
+      const matches = result.matches
+      const scoreLen = matches.length
+
+      let totalWeightedScore = 1
+
+      for (let j = 0; j < scoreLen; j += 1) {
+        const item = matches[j]
+        const key = item.key
+        const keyWeight = this._keyStore.get(key, 'weight')
+        const weight = keyWeight > -1 ? keyWeight : 1
+        const score = item.score === 0 && keyWeight > -1
+          ? Number.EPSILON
+          : item.score
+
+        totalWeightedScore *= Math.pow(score, weight)
+      }
+
+      result.score = totalWeightedScore
+    }
+  }
+
+  _sort(results) {
+    results.sort(this.options.sortFn)
+  }
+
+  _format(results) {
+    const finalOutput = []
+
+    const { includeMatches, includeScore, } = this.options
+
+    let transformers = []
+
+    if (includeMatches) transformers.push(transformMatches)
+    if (includeScore) transformers.push(transformScore)
+
+    for (let i = 0, len = results.length; i < len; i += 1) {
+      const result = results[i]
+      const { idx } = result
+
+      const data = {
+        item: this.list[idx],
+        refIndex: idx
+      }
+
+      if (transformers.length) {
+        for (let j = 0, len = transformers.length; j < len; j += 1) {
+          transformers[j](result, data)
+        }
+      }
+
+      finalOutput.push(data)
+    }
+
+    return finalOutput
+  }
+}

+ 3 - 3
src/helpers/get.js

@@ -1,12 +1,12 @@
-const {
+import {
   isDefined,
   isString,
   isNumber,
   isArray,
   toString
-} = require('./type-checkers')
+} from './type-checkers'
 
-module.exports = (obj, path) => {
+export default function get(obj, path) {
   let list = []
   let arr = false
 

+ 7 - 16
src/helpers/type-checkers.js

@@ -1,12 +1,12 @@
 const INFINITY = 1 / 0
 
-const isArray = value => !Array.isArray
+export const isArray = value => !Array.isArray
   ? Object.prototype.toString.call(value) === '[object Array]'
   : Array.isArray(value)
 
 // Adapted from:
 // https://github.com/lodash/lodash/blob/f4ca396a796435422bd4fd41fadbd225edddf175/.internal/baseToString.js
-const baseToString = value => {
+export const baseToString = value => {
   // Exit early for strings to avoid a performance hit in some environments.
   if (typeof value == 'string') {
     return value;
@@ -15,21 +15,12 @@ const baseToString = value => {
   return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
 }
 
-const toString = value => value == null ? '' : baseToString(value);
+export const toString = value => value == null ? '' : baseToString(value);
 
-const isString = value => typeof value === 'string'
+export const isString = value => typeof value === 'string'
 
-const isNumber = value => typeof value === 'number'
+export const isNumber = value => typeof value === 'number'
 
-const isObject = value => typeof value === 'object'
+export const isObject = value => typeof value === 'object'
 
-const isDefined = value => value !== undefined && value !== null
-
-module.exports = {
-  isDefined,
-  isArray,
-  isString,
-  isNumber,
-  isObject,
-  toString
-}
+export const isDefined = value => value !== undefined && value !== null

+ 221 - 0
src/index.d.ts

@@ -0,0 +1,221 @@
+export = Fuse
+export as namespace Fuse
+
+declare class Fuse<T, O extends Fuse.IFuseOptions<T>> {
+  constructor(
+    list: ReadonlyArray<T>,
+    options?: O,
+    index?: ReadonlyArray<Fuse.FuseIndexRecord>,
+  )
+  /**
+   * Search function for the Fuse instance.
+   *
+   * ```typescript
+   * const list: MyType[] = [myType1, myType2, etc...]
+
+   * const options: Fuse.IFuseOptions<MyType> = {
+   *  keys: ['key1', 'key2']
+   * }
+   *
+   * const myFuse = new Fuse(list, options)
+   * let result = myFuse.search('pattern')
+   * ```
+   *
+   * @param pattern The pattern to search
+   * @param options `Fuse.FuseSearchOptions`
+   * @returns An array of search results
+   */
+  search<R = T>(
+    pattern: string,
+    options?: Fuse.FuseSearchOptions,
+  ): Fuse.FuseResult<R>[]
+
+  setCollection(
+    list: ReadonlyArray<T>,
+    index?: ReadonlyArray<Fuse.FuseIndexRecord>,
+  ): void
+
+  setIndex(index: ReadonlyArray<Fuse.FuseIndexRecord>): void
+
+  /**
+   * Return the current version
+   */
+  static version: string
+
+  /**
+   * Use this method to pre-generate the index from the list, and pass it
+   * directly into the Fuse instance.
+   *
+   * _Note that Fuse will automatically index the table if one isn't provided
+   * during instantiation._
+   *
+   * ```typescript
+   * const list: MyType[] = [myType1, myType2, etc...]
+   *
+   * const index = Fuse.createIndex<MyType>(
+   *  keys: ['key1', 'key2']
+   *  list: list
+   * )
+   *
+   * const options: Fuse.IFuseOptions<MyType> = {
+   *  keys: ['key1', 'key2']
+   * }
+   *
+   * const myFuse = new Fuse(list, options, index)
+   * ```
+   * @param keys    The keys to index
+   * @param list    The list from which to create an index
+   * @param options?
+   * @returns An indexed list
+   */
+  static createIndex<U>(
+    keys: Fuse.FuseOptionKeyObject[] | string[],
+    list: ReadonlyArray<U>,
+    options?: Fuse.FuseIndexOptions<U>,
+  ): ReadonlyArray<Fuse.FuseIndexRecord>
+}
+
+declare namespace Fuse {
+  type FuseGetFunction<T> = (
+    obj: T,
+    path: string,
+  ) => ReadonlyArray<string> | string
+
+  export type FuseIndexOptions<T> = {
+    getFn: FuseGetFunction<T>
+    ngrams: boolean
+  }
+
+  // {
+  //   title: { '$': "Old Man's War" },
+  //   'author.firstName': { '$': 'Codenar' }
+  // }
+  //
+  // OR
+  //
+  // {
+  //   tags: [
+  //     { $: 'nonfiction', idx: 1 },
+  //     { $: 'web development', idx: 0 },
+  //   ]
+  // }
+  export type FuseSortFunctionItem = {
+    [key: string]: { $: string } | { $: string; idx: number }[]
+  }
+
+  // {
+  //   score: 0.001,
+  //   key: 'author.firstName',
+  //   value: 'Codenar',
+  //   indices: [ [ 0, 3 ] ]
+  // }
+  export type FuseSortFunctionMatch = {
+    score: number
+    key: string
+    value: string
+    indices: ReadonlyArray<number>[]
+  }
+
+  // {
+  //   score: 0,
+  //   key: 'tags',
+  //   value: 'nonfiction',
+  //   idx: 1,
+  //   indices: [ [ 0, 9 ] ]
+  // }
+  export type FuseSortFunctionMatchList = FuseSortFunctionMatch & {
+    idx: number
+  }
+
+  export type FuseSortFunctionArg = {
+    idx: number
+    item: FuseSortFunctionItem
+    score: number
+    matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]
+  }
+
+  export type FuseSortFunction = (
+    a: FuseSortFunctionArg,
+    b: FuseSortFunctionArg,
+  ) => number
+
+  // title: {
+  //   '$': "Old Man's War",
+  //   ng: [
+  //     ' ma', ' ol', ' wa',
+  //     "'s ", "an'", 'ar ',
+  //     'd m', 'ld ', 'man',
+  //     "n's", 'old', 's w',
+  //     'war'
+  //   ]
+  // }
+  type RecordEntryObject = { $: string; ng?: ReadonlyArray<string> }
+
+  // 'author.tags.name': [{
+  //   '$': 'pizza lover',
+  //   idx: 2,
+  //   ng: [
+  //     ' lo', ' pi', 'a l',
+  //     'er ', 'izz', 'lov',
+  //     'ove', 'piz', 'ver',
+  //     'za ', 'zza'
+  //   ]
+  // }
+  type RecordEntryArrayItem = ReadonlyArray<RecordEntryObject & { idx: number }>
+
+  // TODO: this makes it difficult to infer the type. Need to think more about this
+  type RecordEntry = { [key: string]: RecordEntryObject | RecordEntryArrayItem }
+
+  type FuseIndexRecord = {
+    idx: number
+    $: RecordEntry
+  }
+
+  // {
+  //   name: 'title',
+  //   weight: 0.7
+  // }
+  export type FuseOptionKeyObject = {
+    name: string
+    weight: number
+  }
+
+  export interface IFuseOptions<T> {
+    caseSensitive?: boolean
+    distance?: number
+    findAllMatches?: boolean
+    getFn?: FuseGetFunction<T>
+    includeMatches?: boolean
+    includeScore?: boolean
+    keys?: FuseOptionKeyObject[] | string[]
+    location?: number
+    minMatchCharLength?: number
+    shouldSort?: boolean
+    sortFn?: FuseSortFunction
+    threshold?: number
+    useExtendedSearch?: boolean
+  }
+
+  // Denotes the start/end indices of a match
+  //                 start    end
+  //                   ↓       ↓
+  type RangeTuple = [number, number]
+
+  export type FuseResultMatch = {
+    indices: ReadonlyArray<RangeTuple>[]
+    key?: string
+    refIndex?: number
+    value?: string
+  }
+
+  export type FuseSearchOptions = {
+    limit: number
+  }
+
+  export type FuseResult<T> = {
+    item: T
+    refIndex: number
+    score?: number
+    matches?: ReadonlyArray<FuseResultMatch>
+  }
+}

+ 4 - 366
src/index.js

@@ -1,369 +1,7 @@
+import Fuse from './core'
+import { createIndex } from './tools'
 
-const { BitapSearch, ExtendedSearch, NGramSearch } = require('./search')
-const { isArray, isDefined, isString, isNumber, isObject } = require('./helpers/type-checkers')
-const get = require('./helpers/get')
-const { createIndex, KeyStore } = require('./tools')
-const { transformMatches, transformScore } = require('./transform')
-const { MAX_BITS } = require('./search/bitap-search/constants')
-
-// // Will print to the console. Useful for debugging.
-// function debug() {
-//   if (Fuse.verbose) {
-//     console.log(...arguments)
-//     // const util = require('util')
-//     // console.log(util.inspect(...arguments, false, null, true /* enable colors */))
-//   }
-// }
-
-// function debugTime(value) {
-//   if (Fuse.verboseTime) {
-//     console.time(value)
-//   }
-// }
-
-// function debugTimeEnd(value) {
-//   if (Fuse.verboseTime) {
-//     console.timeEnd(value)
-//   }
-// }
-
-let FuseOptions = {
-  // When true, the algorithm continues searching to the end of the input even if a perfect
-  // match is found before the end of the same input.
-  isCaseSensitive: false,
-  // Determines how close the match must be to the fuzzy location (specified above).
-  // An exact letter match which is 'distance' characters away from the fuzzy location
-  // would score as a complete mismatch. A distance of '0' requires the match be at
-  // the exact location specified, a threshold of '1000' would require a perfect match
-  // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
-  distance: 100,
-  // Minimum number of characters that must be matched before a result is considered a match
-  findAllMatches: false,
-  // The get function to use when fetching an object's properties.
-  // The default will search nested paths *ie foo.bar.baz*
-  getFn: get,
-  includeMatches: false,
-  includeScore: false,
-  // List of properties that will be searched. This also supports nested properties.
-  keys: [],
-  // Approximately where in the text is the pattern expected to be found?
-  location: 0,
-  // Minimum number of characters that must be matched before a result is considered a match
-  minMatchCharLength: 1,
-  // Whether to sort the result list, by score
-  shouldSort: true,
-  // Default sort function
-  sortFn: (a, b) => (a.score - b.score),
-  // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
-  // (of both letters and location), a threshold of '1.0' would match anything.
-  threshold: 0.6,
-  // Enabled extended-searching
-  useExtendedSearch: false
-}
-
-class Fuse {
-  constructor(list, options = FuseOptions, index = null) {
-    this.options = { ...FuseOptions, ...options }
-    // `caseSensitive` is deprecated, use `isCaseSensitive` instead
-    this.options.isCaseSensitive = options.caseSensitive
-    delete this.options.caseSensitive
-
-    // debugTime('Constructing')
-    this._processKeys(this.options.keys)
-    this.setCollection(list, index)
-    // debugTimeEnd('Constructing')
-  }
-
-  setCollection(list, index = null) {
-    this.list = list
-    this.listIsStringArray = isString(list[0])
-
-    if (index) {
-      this.setIndex(index)
-    } else {
-      // debugTime('Process index')
-      this.setIndex(this._createIndex())
-      // debugTimeEnd('Process index')
-    }
-  }
-
-  setIndex(listIndex) {
-    this._indexedList = listIndex
-    // debug(listIndex)
-  }
-
-  _processKeys(keys) {
-    this._keyStore = new KeyStore(keys)
-
-    // debug('Process Keys')
-    if (Fuse.verbose) {
-      // debug(this._keyStore.toJSON())
-    }
-  }
-
-  _createIndex() {
-    return createIndex(this._keyStore.keys(), this.list, {
-      getFn: this.options.getFn
-    })
-  }
-
-  search(pattern, opts = { limit: false }) {
-    // debug(`--------- Search pattern: "${pattern}"`)
-    const { useExtendedSearch, shouldSort } = this.options
-
-    let searcher = null
-
-    if (useExtendedSearch) {
-      searcher = new ExtendedSearch(pattern, this.options)
-    } else if (pattern.length > MAX_BITS) {
-      searcher = new NGramSearch(pattern, this.options)
-    } else {
-      searcher = new BitapSearch(pattern, this.options)
-    }
-
-    // debugTime('Search time');
-    let results = this._searchUsing(searcher)
-    // debugTimeEnd('Search time');
-
-    // debugTime('Compute score time');
-    this._computeScore(results)
-    // debugTimeEnd('Compute score time');
-
-    if (shouldSort) {
-      this._sort(results)
-    }
-
-    if (opts.limit && isNumber(opts.limit)) {
-      results = results.slice(0, opts.limit)
-    }
-
-    return this._format(results)
-  }
-
-  _searchUsing(searcher) {
-    const list = this._indexedList
-    const results = []
-    const { includeMatches } = this.options
-
-    // List is Array<String>
-    if (this.listIsStringArray) {
-      // Iterate over every string in the list
-      for (let i = 0, len = list.length; i < len; i += 1) {
-        let value = list[i]
-        let { $: text, idx } = value
-
-        if (!isDefined(text)) {
-          continue
-        }
-
-        let searchResult = searcher.searchIn(value)
-
-        const { isMatch, score } = searchResult
-
-        if (!isMatch) {
-          continue
-        }
-
-        let match = { score, value: text }
-
-        if (includeMatches) {
-          match.indices = searchResult.matchedIndices
-        }
-
-        results.push({
-          item: text,
-          idx,
-          matches: [match]
-        })
-      }
-
-    } else {
-      // List is Array<Object>
-      const keyNames = this._keyStore.keys()
-      const keysLen = this._keyStore.count()
-
-      for (let i = 0, len = list.length; i < len; i += 1) {
-        let { $: item, idx } = list[i]
-
-        if (!isDefined(item)) {
-          continue
-        }
-
-        let matches = []
-
-        // Iterate over every key (i.e, path), and fetch the value at that key
-        for (let j = 0; j < keysLen; j += 1) {
-          let key = keyNames[j]
-          let value = item[key]
-
-          // debug(` Key: ${key === '' ? '--' : key}`)
-
-          if (!isDefined(value)) {
-            continue
-          }
-
-          if (isArray(value)) {
-            for (let k = 0, len = value.length; k < len; k += 1) {
-              let arrItem = value[k]
-              let text = arrItem.$
-              let idx = arrItem.idx
-
-              if (!isDefined(text)) {
-                continue
-              }
-
-              let searchResult = searcher.searchIn(arrItem)
-
-              const { isMatch, score } = searchResult
-
-              // debug(`Full text: "${text}", score: ${score}`)
-
-              if (!isMatch) {
-                continue
-              }
-
-              let match = { score, key, value: text, idx }
-
-              if (includeMatches) {
-                match.indices = searchResult.matchedIndices
-              }
-
-              matches.push(match)
-            }
-          } else {
-            let text = value.$
-            let searchResult = searcher.searchIn(value)
-
-            const { isMatch, score } = searchResult
-
-            // debug(`Full text: "${text}", score: ${score}`)
-
-            if (!isMatch) {
-              continue
-            }
-
-            let match = { score, key, value: text }
-
-            if (includeMatches) {
-              match.indices = searchResult.matchedIndices
-            }
-
-            matches.push(match)
-          }
-        }
-
-        if (matches.length) {
-          results.push({
-            idx,
-            item,
-            matches
-          })
-        }
-      }
-    }
-
-    // debug("--------- RESULTS -----------")
-    // debug(results)
-    // debug("-----------------------------")
-
-    return results
-  }
-
-  _computeScore(results) {
-    // debug('Computing score: ')
-
-    for (let i = 0, len = results.length; i < len; i += 1) {
-      const result = results[i]
-      const matches = result.matches
-      const scoreLen = matches.length
-
-      let totalWeightedScore = 1
-      // let bestScore = -1
-
-      for (let j = 0; j < scoreLen; j += 1) {
-        const item = matches[j]
-        const key = item.key
-        const keyWeight = this._keyStore.get(key, 'weight')
-        const weight = keyWeight > -1 ? keyWeight : 1