UNPKG

quick-score

Version:

A JavaScript string-scoring and fuzzy-matching library based on the Quicksilver algorithm, designed for smart auto-complete.

1,053 lines (921 loc) 40.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.quickScore = {})); })(this, (function (exports) { 'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } /** * A class representing a half-open interval of characters. A range's `location` * property and `max()` value can be used as arguments for the `substring()` * method to extract a range of characters. */ var Range = /*#__PURE__*/function () { /** * @memberOf Range.prototype * @member {number} location Starting index of the range. */ /** * @memberOf Range.prototype * @member {number} length Number of characters in the range. */ /** * @param {number} [location=-1] Starting index of the range. * @param {number} [length=0] Number of characters in the range. */ function Range() { var location = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : -1; var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; _classCallCheck(this, Range); this.location = location; this.length = length; } /* eslint no-inline-comments: 0 */ /** * Gets the end index of the range, which indicates the character * immediately after the last one in the range. * * @returns {number} */ /** * Sets the end index of the range, which indicates the character * immediately after the last one in the range. * * @param {number} [value] End of the range. * * @returns {number} */ _createClass(Range, [{ key: "max", value: function max(value) { if (typeof value == "number") { this.length = value - this.location; } // the NSMaxRange() function in Objective-C returns this value return this.location + this.length; } /** * Returns whether the range contains a location >= 0. * * @returns {boolean} */ }, { key: "isValid", value: function isValid() { return this.location > -1; } /** * Returns an array of the range's start and end indexes. * * @returns {RangeTuple} */ }, { key: "toArray", value: function toArray() { return [this.location, this.max()]; } /** * Returns a string representation of the range's open interval. * * @returns {string} */ }, { key: "toString", value: function toString() { if (this.location == -1) { return "invalid range"; } else { return "[" + this.location + "," + this.max() + ")"; } } }]); return Range; }(); var BaseConfigDefaults = { wordSeparators: "-/\\:()<>%._=&[]+ \t\n\r", uppercaseLetters: function () { var charCodeA = "A".charCodeAt(0); var uppercase = []; for (var i = 0; i < 26; i++) { uppercase.push(String.fromCharCode(charCodeA + i)); } return uppercase.join(""); }(), ignoredScore: 0.9, skippedScore: 0.15, emptyQueryScore: 0, // long, nearly-matching queries can generate up to 2^queryLength loops, // so support worst-case queries up to 16 characters and then give up // and return 0 for longer queries that may or may not actually match maxIterations: Math.pow(2, 16) }; var QSConfigDefaults = { longStringLength: 150, maxMatchStartPct: 0.15, minMatchDensityPct: 0.75, maxMatchDensityPct: 0.95, beginningOfStringPct: 0.1 }; var Config = /*#__PURE__*/function () { function Config(options) { _classCallCheck(this, Config); Object.assign(this, BaseConfigDefaults, options); } _createClass(Config, [{ key: "useSkipReduction", value: function useSkipReduction() { return true; } }, { key: "adjustRemainingScore", value: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) { // use the original Quicksilver expression for the remainingScore return remainingScore * remainingSearchRange.length; } }]); return Config; }(); var QuickScoreConfig = /*#__PURE__*/function (_Config) { _inherits(QuickScoreConfig, _Config); var _super = _createSuper(QuickScoreConfig); function QuickScoreConfig(options) { _classCallCheck(this, QuickScoreConfig); return _super.call(this, Object.assign({}, QSConfigDefaults, options)); } _createClass(QuickScoreConfig, [{ key: "useSkipReduction", value: function useSkipReduction(string, query, remainingScore, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) { var len = string.length; var isShortString = len <= this.longStringLength; var matchStartPercentage = fullMatchedRange.location / len; return isShortString || matchStartPercentage < this.maxMatchStartPct; } }, { key: "adjustRemainingScore", value: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) { var isShortString = string.length <= this.longStringLength; var matchStartPercentage = fullMatchedRange.location / string.length; var matchRangeDiscount = 1; var matchStartDiscount = 1 - matchStartPercentage; // discount the remainingScore based on how much larger the match is // than the query, unless the match is in the first 10% of the // string, the match range isn't too sparse and the whole string is // not too long. also only discount if we didn't skip any whitespace // or capitals. if (!skippedSpecialChar) { matchRangeDiscount = query.length / fullMatchedRange.length; matchRangeDiscount = isShortString && matchStartPercentage <= this.beginningOfStringPct && matchRangeDiscount >= this.minMatchDensityPct ? 1 : matchRangeDiscount; matchStartDiscount = matchRangeDiscount >= this.maxMatchDensityPct ? 1 : matchStartDiscount; } // discount the scores of very long strings return remainingScore * Math.min(remainingSearchRange.length, this.longStringLength) * matchRangeDiscount * matchStartDiscount; } }]); return QuickScoreConfig; }(Config); function createConfig(options) { if (options instanceof Config) { // this is a full-fledged Config instance, so we don't need to do // anything to it return options; } else { // create a complete config from this return new QuickScoreConfig(options); } } var DefaultConfig = createConfig(); var BaseConfig = new Config(); var QuicksilverConfig = new Config({ // the Quicksilver algorithm returns .9 for empty queries emptyQueryScore: 0.9, adjustRemainingScore: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) { var score = remainingScore * remainingSearchRange.length; if (!skippedSpecialChar) { // the current QuickSilver algorithm reduces the score by half // this value when no special chars are skipped, so add the half // back in to match it score += (matchedRange.location - searchRange.location) / 2.0; } return score; } }); /** * Scores a string against a query. * * @param {string} string The string to score. * * @param {string} query The query string to score the `string` parameter against. * * @param {Array<RangeTuple>} [matches] If supplied, `quickScore()` will push onto * `matches` an array with start and end indexes for each substring range of * `string` that matches `query`. These indexes can be used to highlight the * matching characters in an auto-complete UI. * * @param {string} [transformedString] A transformed version of the string that * will be used for matching. This defaults to a lowercase version of `string`, * but it could also be used to match against a string with all the diacritics * removed, so an unaccented character in the query would match an accented one * in the string. * * @param {string} [transformedQuery] A transformed version of `query`. The * same transformation applied to `transformedString` should be applied to this * parameter, or both can be left as `undefined` for the default lowercase * transformation. * * @param {object} [config] A configuration object that can modify how the * `quickScore` algorithm behaves. * * @param {Range} [stringRange] The range of characters in `string` that should * be checked for matches against `query`. Defaults to the entire `string` * parameter. * * @returns {number} A number between 0 and 1 that represents how well the * `query` matches the `string`. */ function quickScore(string, query, matches) { var transformedString = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : string.toLocaleLowerCase(); var transformedQuery = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : query.toLocaleLowerCase(); var config = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : DefaultConfig; var stringRange = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : new Range(0, string.length); var iterations = 0; if (query) { return calcScore(stringRange, new Range(0, query.length), new Range()); } else { return config.emptyQueryScore; } function calcScore(searchRange, queryRange, fullMatchedRange) { if (!queryRange.length) { // deduct some points for all remaining characters return config.ignoredScore; } else if (queryRange.length > searchRange.length) { return 0; } var initialMatchesLength = matches && matches.length; for (var i = queryRange.length; i > 0; i--) { if (iterations > config.maxIterations) { // a long query that matches the string except for the last // character can generate 2^queryLength iterations of this // loop before returning 0, so short-circuit that when we've // seen too many iterations (bit of an ugly kludge, but it // avoids locking up the UI if the user somehow types an // edge-case query) return 0; } iterations++; var querySubstring = transformedQuery.substring(queryRange.location, queryRange.location + i); // reduce the length of the search range by the number of chars // we're skipping in the query, to make sure there's enough string // left to possibly contain the skipped chars var matchedRange = getRangeOfSubstring(transformedString, querySubstring, new Range(searchRange.location, searchRange.length - queryRange.length + i)); if (!matchedRange.isValid()) { // we didn't find the query substring, so try again with a // shorter substring continue; } if (!fullMatchedRange.isValid()) { fullMatchedRange.location = matchedRange.location; } else { fullMatchedRange.location = Math.min(fullMatchedRange.location, matchedRange.location); } fullMatchedRange.max(matchedRange.max()); if (matches) { matches.push(matchedRange.toArray()); } var remainingSearchRange = new Range(matchedRange.max(), searchRange.max() - matchedRange.max()); var remainingQueryRange = new Range(queryRange.location + i, queryRange.length - i); var remainingScore = calcScore(remainingSearchRange, remainingQueryRange, fullMatchedRange); if (remainingScore) { var score = remainingSearchRange.location - searchRange.location; // default to true since we only want to apply a discount if // we hit the final else clause below, and we won't get to // any of them if the match is right at the start of the // searchRange var skippedSpecialChar = true; var useSkipReduction = config.useSkipReduction(string, query, remainingScore, remainingSearchRange, searchRange, remainingSearchRange, matchedRange, fullMatchedRange); if (matchedRange.location > searchRange.location) { // some letters were skipped when finding this match, so // adjust the score based on whether spaces or capital // letters were skipped if (useSkipReduction && config.wordSeparators.indexOf(string[matchedRange.location - 1]) > -1) { for (var j = matchedRange.location - 2; j >= searchRange.location; j--) { if (config.wordSeparators.indexOf(string[j]) > -1) { score--; } else { score -= config.skippedScore; } } } else if (useSkipReduction && config.uppercaseLetters.indexOf(string[matchedRange.location]) > -1) { for (var _j = matchedRange.location - 1; _j >= searchRange.location; _j--) { if (config.uppercaseLetters.indexOf(string[_j]) > -1) { score--; } else { score -= config.skippedScore; } } } else { // reduce the score by the number of chars we've // skipped since the beginning of the search range score -= matchedRange.location - searchRange.location; skippedSpecialChar = false; } } score += config.adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange); score /= searchRange.length; return score; } else if (matches) { // the remaining query does not appear in the remaining // string, so strip off any matches we've added during the // current call, as they'll be invalid when we start over // with a shorter piece of the query matches.length = initialMatchesLength; } } return 0; } } // make createConfig() available on quickScore so that the QuickScore // constructor has access to it quickScore.createConfig = createConfig; function getRangeOfSubstring(string, query, searchRange) { var index = string.indexOf(query, searchRange.location); var result = new Range(); if (index > -1 && index < searchRange.max()) { result.location = index; result.length = query.length; } return result; } /** * A class for scoring and sorting a list of items against a query string. Each * item receives a floating point score between `0` and `1`. */ var QuickScore = /*#__PURE__*/function () { /** * @memberOf QuickScore.prototype * @member {Array<object>} items The array of items to search, which * should only be modified via the [setItems()]{@link QuickScore#setItems} * method. * @readonly */ /** * @memberOf QuickScore.prototype * @member {Array<ItemKey>} keys The keys to search on each item, which * should only be modified via the [setItems()]{@link QuickScore#setKeys} * method. * @readonly */ /** * @param {Array<string|object>} [items] The list of items to score. If * the list is not a flat array of strings, a `keys` array must be supplied * via the second parameter. QuickScore makes a shallow copy of the `items` * array, so changes to it won't have any affect, but changes to the objects * referenced by the array need to be passed to the instance by a call to * its [setItems()]{@link QuickScore#setItems} method. * * @param {Array<ItemKey>|Options} [options] If the `items` parameter * is an array of flat strings, the `options` parameter can be left out. If * it is a list of objects containing keys that should be scored, the * `options` parameter must either be an array of key names or an object * containing a `keys` property. * * @param {Array<ItemKey>} [options.keys] In the simplest case, an array of * key names to score on the objects in the `items` array. * * The key names can point to a nested key by passing either a dot-delimited * string or an array of sub-keys that specify the path to the value. So a * key `name` of `"foo.bar"` would evaluate to `"baz"` given an object like * `{ foo: { bar: "baz" } }`. Alternatively, that path could be passed as * an array, like `["foo", "bar"]`. In either case, if this sub-key's match * produces the highest score for an item in the search results, its * `scoreKey` name will be `"foo.bar"`. * * If your items have keys that contain periods, e.g., `"first.name"`, but * you don't want these names to be treated as paths to nested keys, simply * wrap the name in an array, like `{ keys: ["ssn", ["first.name"], * ["last.name"]] }`. * * Instead of a string or string array, an item in `keys` can also be passed * as a `{name, scorer}` object, which lets you specify a different scoring * function for each key. The scoring function should behave as described * next. * * @param {string} [options.sortKey=options.keys[0]] An optional key name * that will be used to sort items with identical scores. Defaults to the * name of the first item in the `keys` parameter. If `sortKey` points to * a nested key, use a dot-delimited string instead of an array to specify * the path. * * @param {number} [options.minimumScore=0] An optional value that * specifies the minimum score an item must have to appear in the results * returned from [search()]{@link QuickScore#search}. Defaults to `0`, * so items that don't match the full `query` will not be returned. This * value is ignored if the `query` is empty or undefined, in which case all * items are returned, sorted alphabetically and case-insensitively on the * `sortKey`, if any. * * @param {TransformStringFunction} [options.transformString] An optional * function that takes a `string` parameter and returns a transformed * version of that string. This function will be called on each of the * searchable keys in the `items` array as well as on the `query` * parameter to the `search()` method. The default function calls * `toLocaleLowerCase()` on each string, for a case-insensitive search. The * result of this function is cached for each searchable key on each item. * * You can pass a function here to do other kinds of preprocessing, such as * removing diacritics from all the strings or converting Chinese characters * to pinyin. For example, you could use the * [`latinize`](https://www.npmjs.com/package/latinize) npm package to * convert characters with diacritics to the base character so that your * users can type an unaccented character in the query while still matching * items that have accents or diacritics. Pass in an `options` object like * this to use a custom `transformString()` function: * `{ transformString: s => latinize(s.toLocaleLowerCase()) }` * * @param {ScorerFunction} [options.scorer] An optional function that takes * `string` and `query` parameters and returns a floating point number * between 0 and 1 that represents how well the `query` matches the * `string`. It defaults to the [quickScore()]{@link quickScore} function * in this library. * * If the function gets a third `matches` parameter, it should fill the * passed-in array with indexes corresponding to where the query * matches the string, as described in the [search()]{@link QuickScore#search} * method. * * @param {Config} [options.config] An optional object that is passed to * the scorer function to further customize its behavior. If the * `scorer` function has a `createConfig()` method on it, the `QuickScore` * instance will call that with the `config` value and store the result. * This can be used to extend the `config` parameter with default values. */ function QuickScore() { var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, QuickScore); var _ref = Array.isArray(options) ? { keys: options } : options, _ref$scorer = _ref.scorer, scorer = _ref$scorer === void 0 ? quickScore : _ref$scorer, _ref$transformString = _ref.transformString, transformString = _ref$transformString === void 0 ? toLocaleLowerCase : _ref$transformString, _ref$keys = _ref.keys, keys = _ref$keys === void 0 ? [] : _ref$keys, _ref$sortKey = _ref.sortKey, sortKey = _ref$sortKey === void 0 ? "" : _ref$sortKey, _ref$minimumScore = _ref.minimumScore, minimumScore = _ref$minimumScore === void 0 ? 0 : _ref$minimumScore, config = _ref.config; this.scorer = scorer; this.minimumScore = minimumScore; this.config = config; this.transformStringFunc = transformString; if (typeof scorer.createConfig === "function") { // let the scorer fill out the config with default values this.config = scorer.createConfig(config); } this.setKeys(keys, sortKey); this.setItems(items); // the scoring function needs access to this.sortKey this.compareScoredStrings = this.compareScoredStrings.bind(this); } /** * Scores the instance's items against the `query` and sorts them from * highest to lowest. * * @param {string} query The string to score each item against. The * instance's `transformString()` function is called on this string before * it's matched against each item. * * @returns {Array<ScoredString|ScoredObject>} When the instance's `items` * are flat strings, an array of [`ScoredString`]{@link ScoredString} * objects containing the following properties is returned: * * - `item`: the string that was scored * - `score`: the floating point score of the string for the current query * - `matches`: an array of arrays that specify the character ranges * where the query matched the string * * When the `items` are objects, an array of [`ScoredObject`]{@link ScoredObject} * results is returned: * * - `item`: the object that was scored * - `score`: the highest score from among the individual key scores * - `scoreKey`: the name of the key with the highest score, which will be * an empty string if they're all zero * - `scoreValue`: the value of the key with the highest score, which makes * it easier to access if it's a nested string * - `scores`: a hash of the individual scores for each key * - `matches`: a hash of arrays that specify the character ranges of the * query match for each key * * The results array is sorted high to low on each item's score. Items with * identical scores are sorted alphabetically and case-insensitively on the * `sortKey` option. Items with scores that are <= the `minimumScore` option * (defaults to `0`) are not returned, unless the `query` is falsy, in which * case all of the items are returned, sorted alphabetically. * * The start and end indices in each [`RangeTuple`]{@link RangeTuple} in the * `matches` array can be used as parameters to the `substring()` method to * extract the characters from each string that match the query. This can * then be used to format the matching characters with a different color or * style. * * Each `ScoredObject` item also has a `_` property, which caches transformed * versions of the item's strings, and might contain additional internal * metadata in the future. It can be ignored. */ _createClass(QuickScore, [{ key: "search", value: function search(query) { var results = []; var items = this.items, transformedItems = this.transformedItems, sharedKeys = this.keys, config = this.config; // if the query is empty, we want to return all items, so make the // minimum score less than 0 var minScore = query ? this.minimumScore : -1; var transformedQuery = this.transformString(query); var itemCount = items.length; var sharedKeyCount = sharedKeys.length; if (typeof items[0] === "string") { // items is an array of strings for (var i = 0; i < itemCount; i++) { var item = items[i]; var transformedItem = transformedItems[i]; var matches = []; var score = this.scorer(item, query, matches, transformedItem, transformedQuery, config); if (score > minScore) { results.push({ item: item, score: score, matches: matches, _: transformedItem }); } } } else { for (var _i = 0; _i < itemCount; _i++) { var _item = items[_i]; var _transformedItem = transformedItems[_i]; var result = { item: _item, score: 0, scoreKey: "", scoreValue: "", scores: {}, matches: {}, _: _transformedItem }; // if an empty keys array was passed into the constructor, // score all of the non-empty string keys on the object var keys = sharedKeyCount ? sharedKeys : Object.keys(_transformedItem); var keyCount = keys.length; var highScore = 0; var scoreKey = ""; var scoreValue = ""; // find the highest score for each keyed string on this item for (var j = 0; j < keyCount; j++) { var key = keys[j]; // use the key as the name if it's just a string, and // default to the instance's scorer function var _key$name = key.name, name = _key$name === void 0 ? key : _key$name, _key$scorer = key.scorer, scorer = _key$scorer === void 0 ? this.scorer : _key$scorer; var transformedString = _transformedItem[name]; // setItems() checks for non-strings and empty strings // when creating the transformed objects, so if the key // doesn't exist there, we can skip the processing // below for this key in this item if (transformedString) { var string = this.getItemString(_item, key); var _matches = []; var newScore = scorer(string, query, _matches, transformedString, transformedQuery, config); result.scores[name] = newScore; result.matches[name] = _matches; if (newScore > highScore) { highScore = newScore; scoreKey = name; scoreValue = string; } } } if (highScore > minScore) { result.score = highScore; result.scoreKey = scoreKey; result.scoreValue = scoreValue; results.push(result); } } } results.sort(this.compareScoredStrings); return results; } /** * Sets the `keys` configuration. `setItems()` must be called after * changing the keys so that the items' transformed strings get cached. * * @param {Array<ItemKey>} keys List of keys to score, as either strings * or `{name, scorer}` objects. * * @param {string} [sortKey=keys[0]] Name of key on which to sort * identically scored items. Defaults to the first `keys` item. */ }, { key: "setKeys", value: function setKeys(keys, sortKey) { // create a shallow copy of the keys array so that changes to its // order outside of this instance won't affect searching this.keys = keys.slice(); this.sortKey = sortKey; if (this.keys.length) { var scorer = this.scorer; // associate each key with the scorer function, if it isn't already this.keys = this.keys.map(function (itemKey) { // items in the keys array should either be a string or // array specifying a key name, or a { name, scorer } object var key = itemKey.length ? { name: itemKey, scorer: scorer } : itemKey; if (Array.isArray(key.name)) { if (key.name.length > 1) { key.path = key.name; key.name = key.path.join("."); } else { // this path consists of just one key name, which was // probably wrapped in an array because it contains // dots but isn't intended as a key path. so don't // create a path array on this key, so that we're not // constantly calling reduce() to get this one key. var _key$name2 = _slicedToArray(key.name, 1); key.name = _key$name2[0]; } } else if (key.name.indexOf(".") > -1) { key.path = key.name.split("."); } return key; }); this.sortKey = this.sortKey || this.keys[0].name; } } /** * Sets the `items` array and caches a transformed copy of all the item * strings specified by the `keys` parameter to the constructor, using the * `transformString` option (which defaults to `toLocaleLowerCase()`). * * @param {Array<string|object>} items List of items to score. */ }, { key: "setItems", value: function setItems(items) { // create a shallow copy of the items array so that changes to its // order outside of this instance won't affect searching var itemArray = items.slice(); var itemCount = itemArray.length; var transformedItems = []; var sharedKeys = this.keys; var sharedKeyCount = sharedKeys.length; if (typeof itemArray[0] === "string") { for (var i = 0; i < itemCount; i++) { transformedItems.push(this.transformString(itemArray[i])); } } else { for (var _i2 = 0; _i2 < itemCount; _i2++) { var item = itemArray[_i2]; var transformedItem = {}; var keys = sharedKeyCount ? sharedKeys : Object.keys(item); var keyCount = keys.length; for (var j = 0; j < keyCount; j++) { var key = keys[j]; var string = this.getItemString(item, key); if (string && typeof string === "string") { transformedItem[key.name || key] = this.transformString(string); } } transformedItems.push(transformedItem); } } this.items = itemArray; this.transformedItems = transformedItems; } /** * Gets an item's key, possibly at a nested path. * * @private * @param {object} item An object with multiple string properties. * @param {object|string} key A key object with * the name of the string to get from `item`, or a plain string when all * keys on an item are being matched. * @returns {string} */ }, { key: "getItemString", value: function getItemString(item, key) { var name = key.name, path = key.path; if (path) { return path.reduce(function (value, prop) { return value && value[prop]; }, item); } else { // if this instance is scoring all the keys on each item, key // will just be a string, not a { name, scorer } object return item[name || key]; } } /** * Transforms a string into a canonical form for scoring. * * @private * @param {string} string The string to transform. * @returns {string} */ }, { key: "transformString", value: function transformString(string) { return this.transformStringFunc(string); } /** * Compares two items based on their scores, or on their `sortKey` if the * scores are identical. * * @private * @param {object} a First item. * @param {object} b Second item. * @returns {number} */ }, { key: "compareScoredStrings", value: function compareScoredStrings(a, b) { // use the transformed versions of the strings for sorting var itemA = a._; var itemB = b._; var itemAString = typeof itemA === "string" ? itemA : itemA[this.sortKey]; var itemBString = typeof itemB === "string" ? itemB : itemB[this.sortKey]; if (a.score === b.score) { // sort undefineds to the end of the array, as per the ES spec if (itemAString === undefined || itemBString === undefined) { if (itemAString === undefined && itemBString === undefined) { return 0; } else if (itemAString === undefined) { return 1; } else { return -1; } } else if (itemAString === itemBString) { return 0; } else if (itemAString < itemBString) { return -1; } else { return 1; } } else { return b.score - a.score; } } }]); return QuickScore; }(); /** * Default function for transforming each string to be searched. * * @private * @param {string} string The string to transform. * @returns {string} The transformed string. */ function toLocaleLowerCase(string) { return string.toLocaleLowerCase(); } exports.BaseConfig = BaseConfig; exports.DefaultConfig = DefaultConfig; exports.QuickScore = QuickScore; exports.QuicksilverConfig = QuicksilverConfig; exports.Range = Range; exports.createConfig = createConfig; exports.quickScore = quickScore; Object.defineProperty(exports, '__esModule', { value: true }); }));