UNPKG

40.1 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3 typeof define === 'function' && define.amd ? define(['exports'], factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.quickScore = {}));
5})(this, (function (exports) { 'use strict';
6
7 function _classCallCheck(instance, Constructor) {
8 if (!(instance instanceof Constructor)) {
9 throw new TypeError("Cannot call a class as a function");
10 }
11 }
12
13 function _defineProperties(target, props) {
14 for (var i = 0; i < props.length; i++) {
15 var descriptor = props[i];
16 descriptor.enumerable = descriptor.enumerable || false;
17 descriptor.configurable = true;
18 if ("value" in descriptor) descriptor.writable = true;
19 Object.defineProperty(target, descriptor.key, descriptor);
20 }
21 }
22
23 function _createClass(Constructor, protoProps, staticProps) {
24 if (protoProps) _defineProperties(Constructor.prototype, protoProps);
25 if (staticProps) _defineProperties(Constructor, staticProps);
26 Object.defineProperty(Constructor, "prototype", {
27 writable: false
28 });
29 return Constructor;
30 }
31
32 function _inherits(subClass, superClass) {
33 if (typeof superClass !== "function" && superClass !== null) {
34 throw new TypeError("Super expression must either be null or a function");
35 }
36
37 subClass.prototype = Object.create(superClass && superClass.prototype, {
38 constructor: {
39 value: subClass,
40 writable: true,
41 configurable: true
42 }
43 });
44 Object.defineProperty(subClass, "prototype", {
45 writable: false
46 });
47 if (superClass) _setPrototypeOf(subClass, superClass);
48 }
49
50 function _getPrototypeOf(o) {
51 _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
52 return o.__proto__ || Object.getPrototypeOf(o);
53 };
54 return _getPrototypeOf(o);
55 }
56
57 function _setPrototypeOf(o, p) {
58 _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
59 o.__proto__ = p;
60 return o;
61 };
62 return _setPrototypeOf(o, p);
63 }
64
65 function _isNativeReflectConstruct() {
66 if (typeof Reflect === "undefined" || !Reflect.construct) return false;
67 if (Reflect.construct.sham) return false;
68 if (typeof Proxy === "function") return true;
69
70 try {
71 Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
72 return true;
73 } catch (e) {
74 return false;
75 }
76 }
77
78 function _assertThisInitialized(self) {
79 if (self === void 0) {
80 throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
81 }
82
83 return self;
84 }
85
86 function _possibleConstructorReturn(self, call) {
87 if (call && (typeof call === "object" || typeof call === "function")) {
88 return call;
89 } else if (call !== void 0) {
90 throw new TypeError("Derived constructors may only return object or undefined");
91 }
92
93 return _assertThisInitialized(self);
94 }
95
96 function _createSuper(Derived) {
97 var hasNativeReflectConstruct = _isNativeReflectConstruct();
98
99 return function _createSuperInternal() {
100 var Super = _getPrototypeOf(Derived),
101 result;
102
103 if (hasNativeReflectConstruct) {
104 var NewTarget = _getPrototypeOf(this).constructor;
105
106 result = Reflect.construct(Super, arguments, NewTarget);
107 } else {
108 result = Super.apply(this, arguments);
109 }
110
111 return _possibleConstructorReturn(this, result);
112 };
113 }
114
115 function _slicedToArray(arr, i) {
116 return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
117 }
118
119 function _arrayWithHoles(arr) {
120 if (Array.isArray(arr)) return arr;
121 }
122
123 function _iterableToArrayLimit(arr, i) {
124 var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
125
126 if (_i == null) return;
127 var _arr = [];
128 var _n = true;
129 var _d = false;
130
131 var _s, _e;
132
133 try {
134 for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
135 _arr.push(_s.value);
136
137 if (i && _arr.length === i) break;
138 }
139 } catch (err) {
140 _d = true;
141 _e = err;
142 } finally {
143 try {
144 if (!_n && _i["return"] != null) _i["return"]();
145 } finally {
146 if (_d) throw _e;
147 }
148 }
149
150 return _arr;
151 }
152
153 function _unsupportedIterableToArray(o, minLen) {
154 if (!o) return;
155 if (typeof o === "string") return _arrayLikeToArray(o, minLen);
156 var n = Object.prototype.toString.call(o).slice(8, -1);
157 if (n === "Object" && o.constructor) n = o.constructor.name;
158 if (n === "Map" || n === "Set") return Array.from(o);
159 if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
160 }
161
162 function _arrayLikeToArray(arr, len) {
163 if (len == null || len > arr.length) len = arr.length;
164
165 for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
166
167 return arr2;
168 }
169
170 function _nonIterableRest() {
171 throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
172 }
173
174 /**
175 * A class representing a half-open interval of characters. A range's `location`
176 * property and `max()` value can be used as arguments for the `substring()`
177 * method to extract a range of characters.
178 */
179 var Range = /*#__PURE__*/function () {
180 /**
181 * @memberOf Range.prototype
182 * @member {number} location Starting index of the range.
183 */
184
185 /**
186 * @memberOf Range.prototype
187 * @member {number} length Number of characters in the range.
188 */
189
190 /**
191 * @param {number} [location=-1] Starting index of the range.
192 * @param {number} [length=0] Number of characters in the range.
193 */
194 function Range() {
195 var location = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : -1;
196 var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
197
198 _classCallCheck(this, Range);
199
200 this.location = location;
201 this.length = length;
202 }
203 /* eslint no-inline-comments: 0 */
204
205 /**
206 * Gets the end index of the range, which indicates the character
207 * immediately after the last one in the range.
208 *
209 * @returns {number}
210 */
211
212 /**
213 * Sets the end index of the range, which indicates the character
214 * immediately after the last one in the range.
215 *
216 * @param {number} [value] End of the range.
217 *
218 * @returns {number}
219 */
220
221
222 _createClass(Range, [{
223 key: "max",
224 value: function max(value) {
225 if (typeof value == "number") {
226 this.length = value - this.location;
227 } // the NSMaxRange() function in Objective-C returns this value
228
229
230 return this.location + this.length;
231 }
232 /**
233 * Returns whether the range contains a location >= 0.
234 *
235 * @returns {boolean}
236 */
237
238 }, {
239 key: "isValid",
240 value: function isValid() {
241 return this.location > -1;
242 }
243 /**
244 * Returns an array of the range's start and end indexes.
245 *
246 * @returns {RangeTuple}
247 */
248
249 }, {
250 key: "toArray",
251 value: function toArray() {
252 return [this.location, this.max()];
253 }
254 /**
255 * Returns a string representation of the range's open interval.
256 *
257 * @returns {string}
258 */
259
260 }, {
261 key: "toString",
262 value: function toString() {
263 if (this.location == -1) {
264 return "invalid range";
265 } else {
266 return "[" + this.location + "," + this.max() + ")";
267 }
268 }
269 }]);
270
271 return Range;
272 }();
273
274 var BaseConfigDefaults = {
275 wordSeparators: "-/\\:()<>%._=&[]+ \t\n\r",
276 uppercaseLetters: function () {
277 var charCodeA = "A".charCodeAt(0);
278 var uppercase = [];
279
280 for (var i = 0; i < 26; i++) {
281 uppercase.push(String.fromCharCode(charCodeA + i));
282 }
283
284 return uppercase.join("");
285 }(),
286 ignoredScore: 0.9,
287 skippedScore: 0.15,
288 emptyQueryScore: 0,
289 // long, nearly-matching queries can generate up to 2^queryLength loops,
290 // so support worst-case queries up to 16 characters and then give up
291 // and return 0 for longer queries that may or may not actually match
292 maxIterations: Math.pow(2, 16)
293 };
294 var QSConfigDefaults = {
295 longStringLength: 150,
296 maxMatchStartPct: 0.15,
297 minMatchDensityPct: 0.75,
298 maxMatchDensityPct: 0.95,
299 beginningOfStringPct: 0.1
300 };
301
302 var Config = /*#__PURE__*/function () {
303 function Config(options) {
304 _classCallCheck(this, Config);
305
306 Object.assign(this, BaseConfigDefaults, options);
307 }
308
309 _createClass(Config, [{
310 key: "useSkipReduction",
311 value: function useSkipReduction() {
312 return true;
313 }
314 }, {
315 key: "adjustRemainingScore",
316 value: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) {
317 // use the original Quicksilver expression for the remainingScore
318 return remainingScore * remainingSearchRange.length;
319 }
320 }]);
321
322 return Config;
323 }();
324
325 var QuickScoreConfig = /*#__PURE__*/function (_Config) {
326 _inherits(QuickScoreConfig, _Config);
327
328 var _super = _createSuper(QuickScoreConfig);
329
330 function QuickScoreConfig(options) {
331 _classCallCheck(this, QuickScoreConfig);
332
333 return _super.call(this, Object.assign({}, QSConfigDefaults, options));
334 }
335
336 _createClass(QuickScoreConfig, [{
337 key: "useSkipReduction",
338 value: function useSkipReduction(string, query, remainingScore, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) {
339 var len = string.length;
340 var isShortString = len <= this.longStringLength;
341 var matchStartPercentage = fullMatchedRange.location / len;
342 return isShortString || matchStartPercentage < this.maxMatchStartPct;
343 }
344 }, {
345 key: "adjustRemainingScore",
346 value: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) {
347 var isShortString = string.length <= this.longStringLength;
348 var matchStartPercentage = fullMatchedRange.location / string.length;
349 var matchRangeDiscount = 1;
350 var matchStartDiscount = 1 - matchStartPercentage; // discount the remainingScore based on how much larger the match is
351 // than the query, unless the match is in the first 10% of the
352 // string, the match range isn't too sparse and the whole string is
353 // not too long. also only discount if we didn't skip any whitespace
354 // or capitals.
355
356 if (!skippedSpecialChar) {
357 matchRangeDiscount = query.length / fullMatchedRange.length;
358 matchRangeDiscount = isShortString && matchStartPercentage <= this.beginningOfStringPct && matchRangeDiscount >= this.minMatchDensityPct ? 1 : matchRangeDiscount;
359 matchStartDiscount = matchRangeDiscount >= this.maxMatchDensityPct ? 1 : matchStartDiscount;
360 } // discount the scores of very long strings
361
362
363 return remainingScore * Math.min(remainingSearchRange.length, this.longStringLength) * matchRangeDiscount * matchStartDiscount;
364 }
365 }]);
366
367 return QuickScoreConfig;
368 }(Config);
369
370 function createConfig(options) {
371 if (options instanceof Config) {
372 // this is a full-fledged Config instance, so we don't need to do
373 // anything to it
374 return options;
375 } else {
376 // create a complete config from this
377 return new QuickScoreConfig(options);
378 }
379 }
380 var DefaultConfig = createConfig();
381 var BaseConfig = new Config();
382 var QuicksilverConfig = new Config({
383 // the Quicksilver algorithm returns .9 for empty queries
384 emptyQueryScore: 0.9,
385 adjustRemainingScore: function adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange) {
386 var score = remainingScore * remainingSearchRange.length;
387
388 if (!skippedSpecialChar) {
389 // the current QuickSilver algorithm reduces the score by half
390 // this value when no special chars are skipped, so add the half
391 // back in to match it
392 score += (matchedRange.location - searchRange.location) / 2.0;
393 }
394
395 return score;
396 }
397 });
398
399 /**
400 * Scores a string against a query.
401 *
402 * @param {string} string The string to score.
403 *
404 * @param {string} query The query string to score the `string` parameter against.
405 *
406 * @param {Array<RangeTuple>} [matches] If supplied, `quickScore()` will push onto
407 * `matches` an array with start and end indexes for each substring range of
408 * `string` that matches `query`. These indexes can be used to highlight the
409 * matching characters in an auto-complete UI.
410 *
411 * @param {string} [transformedString] A transformed version of the string that
412 * will be used for matching. This defaults to a lowercase version of `string`,
413 * but it could also be used to match against a string with all the diacritics
414 * removed, so an unaccented character in the query would match an accented one
415 * in the string.
416 *
417 * @param {string} [transformedQuery] A transformed version of `query`. The
418 * same transformation applied to `transformedString` should be applied to this
419 * parameter, or both can be left as `undefined` for the default lowercase
420 * transformation.
421 *
422 * @param {object} [config] A configuration object that can modify how the
423 * `quickScore` algorithm behaves.
424 *
425 * @param {Range} [stringRange] The range of characters in `string` that should
426 * be checked for matches against `query`. Defaults to the entire `string`
427 * parameter.
428 *
429 * @returns {number} A number between 0 and 1 that represents how well the
430 * `query` matches the `string`.
431 */
432
433 function quickScore(string, query, matches) {
434 var transformedString = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : string.toLocaleLowerCase();
435 var transformedQuery = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : query.toLocaleLowerCase();
436 var config = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : DefaultConfig;
437 var stringRange = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : new Range(0, string.length);
438 var iterations = 0;
439
440 if (query) {
441 return calcScore(stringRange, new Range(0, query.length), new Range());
442 } else {
443 return config.emptyQueryScore;
444 }
445
446 function calcScore(searchRange, queryRange, fullMatchedRange) {
447 if (!queryRange.length) {
448 // deduct some points for all remaining characters
449 return config.ignoredScore;
450 } else if (queryRange.length > searchRange.length) {
451 return 0;
452 }
453
454 var initialMatchesLength = matches && matches.length;
455
456 for (var i = queryRange.length; i > 0; i--) {
457 if (iterations > config.maxIterations) {
458 // a long query that matches the string except for the last
459 // character can generate 2^queryLength iterations of this
460 // loop before returning 0, so short-circuit that when we've
461 // seen too many iterations (bit of an ugly kludge, but it
462 // avoids locking up the UI if the user somehow types an
463 // edge-case query)
464 return 0;
465 }
466
467 iterations++;
468 var querySubstring = transformedQuery.substring(queryRange.location, queryRange.location + i); // reduce the length of the search range by the number of chars
469 // we're skipping in the query, to make sure there's enough string
470 // left to possibly contain the skipped chars
471
472 var matchedRange = getRangeOfSubstring(transformedString, querySubstring, new Range(searchRange.location, searchRange.length - queryRange.length + i));
473
474 if (!matchedRange.isValid()) {
475 // we didn't find the query substring, so try again with a
476 // shorter substring
477 continue;
478 }
479
480 if (!fullMatchedRange.isValid()) {
481 fullMatchedRange.location = matchedRange.location;
482 } else {
483 fullMatchedRange.location = Math.min(fullMatchedRange.location, matchedRange.location);
484 }
485
486 fullMatchedRange.max(matchedRange.max());
487
488 if (matches) {
489 matches.push(matchedRange.toArray());
490 }
491
492 var remainingSearchRange = new Range(matchedRange.max(), searchRange.max() - matchedRange.max());
493 var remainingQueryRange = new Range(queryRange.location + i, queryRange.length - i);
494 var remainingScore = calcScore(remainingSearchRange, remainingQueryRange, fullMatchedRange);
495
496 if (remainingScore) {
497 var score = remainingSearchRange.location - searchRange.location; // default to true since we only want to apply a discount if
498 // we hit the final else clause below, and we won't get to
499 // any of them if the match is right at the start of the
500 // searchRange
501
502 var skippedSpecialChar = true;
503 var useSkipReduction = config.useSkipReduction(string, query, remainingScore, remainingSearchRange, searchRange, remainingSearchRange, matchedRange, fullMatchedRange);
504
505 if (matchedRange.location > searchRange.location) {
506 // some letters were skipped when finding this match, so
507 // adjust the score based on whether spaces or capital
508 // letters were skipped
509 if (useSkipReduction && config.wordSeparators.indexOf(string[matchedRange.location - 1]) > -1) {
510 for (var j = matchedRange.location - 2; j >= searchRange.location; j--) {
511 if (config.wordSeparators.indexOf(string[j]) > -1) {
512 score--;
513 } else {
514 score -= config.skippedScore;
515 }
516 }
517 } else if (useSkipReduction && config.uppercaseLetters.indexOf(string[matchedRange.location]) > -1) {
518 for (var _j = matchedRange.location - 1; _j >= searchRange.location; _j--) {
519 if (config.uppercaseLetters.indexOf(string[_j]) > -1) {
520 score--;
521 } else {
522 score -= config.skippedScore;
523 }
524 }
525 } else {
526 // reduce the score by the number of chars we've
527 // skipped since the beginning of the search range
528 score -= matchedRange.location - searchRange.location;
529 skippedSpecialChar = false;
530 }
531 }
532
533 score += config.adjustRemainingScore(string, query, remainingScore, skippedSpecialChar, searchRange, remainingSearchRange, matchedRange, fullMatchedRange);
534 score /= searchRange.length;
535 return score;
536 } else if (matches) {
537 // the remaining query does not appear in the remaining
538 // string, so strip off any matches we've added during the
539 // current call, as they'll be invalid when we start over
540 // with a shorter piece of the query
541 matches.length = initialMatchesLength;
542 }
543 }
544
545 return 0;
546 }
547 } // make createConfig() available on quickScore so that the QuickScore
548 // constructor has access to it
549
550 quickScore.createConfig = createConfig;
551
552 function getRangeOfSubstring(string, query, searchRange) {
553 var index = string.indexOf(query, searchRange.location);
554 var result = new Range();
555
556 if (index > -1 && index < searchRange.max()) {
557 result.location = index;
558 result.length = query.length;
559 }
560
561 return result;
562 }
563
564 /**
565 * A class for scoring and sorting a list of items against a query string. Each
566 * item receives a floating point score between `0` and `1`.
567 */
568
569 var QuickScore = /*#__PURE__*/function () {
570 /**
571 * @memberOf QuickScore.prototype
572 * @member {Array<object>} items The array of items to search, which
573 * should only be modified via the [setItems()]{@link QuickScore#setItems}
574 * method.
575 * @readonly
576 */
577
578 /**
579 * @memberOf QuickScore.prototype
580 * @member {Array<ItemKey>} keys The keys to search on each item, which
581 * should only be modified via the [setItems()]{@link QuickScore#setKeys}
582 * method.
583 * @readonly
584 */
585
586 /**
587 * @param {Array<string|object>} [items] The list of items to score. If
588 * the list is not a flat array of strings, a `keys` array must be supplied
589 * via the second parameter. QuickScore makes a shallow copy of the `items`
590 * array, so changes to it won't have any affect, but changes to the objects
591 * referenced by the array need to be passed to the instance by a call to
592 * its [setItems()]{@link QuickScore#setItems} method.
593 *
594 * @param {Array<ItemKey>|Options} [options] If the `items` parameter
595 * is an array of flat strings, the `options` parameter can be left out. If
596 * it is a list of objects containing keys that should be scored, the
597 * `options` parameter must either be an array of key names or an object
598 * containing a `keys` property.
599 *
600 * @param {Array<ItemKey>} [options.keys] In the simplest case, an array of
601 * key names to score on the objects in the `items` array.
602 *
603 * The key names can point to a nested key by passing either a dot-delimited
604 * string or an array of sub-keys that specify the path to the value. So a
605 * key `name` of `"foo.bar"` would evaluate to `"baz"` given an object like
606 * `{ foo: { bar: "baz" } }`. Alternatively, that path could be passed as
607 * an array, like `["foo", "bar"]`. In either case, if this sub-key's match
608 * produces the highest score for an item in the search results, its
609 * `scoreKey` name will be `"foo.bar"`.
610 *
611 * If your items have keys that contain periods, e.g., `"first.name"`, but
612 * you don't want these names to be treated as paths to nested keys, simply
613 * wrap the name in an array, like `{ keys: ["ssn", ["first.name"],
614 * ["last.name"]] }`.
615 *
616 * Instead of a string or string array, an item in `keys` can also be passed
617 * as a `{name, scorer}` object, which lets you specify a different scoring
618 * function for each key. The scoring function should behave as described
619 * next.
620 *
621 * @param {string} [options.sortKey=options.keys[0]] An optional key name
622 * that will be used to sort items with identical scores. Defaults to the
623 * name of the first item in the `keys` parameter. If `sortKey` points to
624 * a nested key, use a dot-delimited string instead of an array to specify
625 * the path.
626 *
627 * @param {number} [options.minimumScore=0] An optional value that
628 * specifies the minimum score an item must have to appear in the results
629 * returned from [search()]{@link QuickScore#search}. Defaults to `0`,
630 * so items that don't match the full `query` will not be returned. This
631 * value is ignored if the `query` is empty or undefined, in which case all
632 * items are returned, sorted alphabetically and case-insensitively on the
633 * `sortKey`, if any.
634 *
635 * @param {TransformStringFunction} [options.transformString] An optional
636 * function that takes a `string` parameter and returns a transformed
637 * version of that string. This function will be called on each of the
638 * searchable keys in the `items` array as well as on the `query`
639 * parameter to the `search()` method. The default function calls
640 * `toLocaleLowerCase()` on each string, for a case-insensitive search. The
641 * result of this function is cached for each searchable key on each item.
642 *
643 * You can pass a function here to do other kinds of preprocessing, such as
644 * removing diacritics from all the strings or converting Chinese characters
645 * to pinyin. For example, you could use the
646 * [`latinize`](https://www.npmjs.com/package/latinize) npm package to
647 * convert characters with diacritics to the base character so that your
648 * users can type an unaccented character in the query while still matching
649 * items that have accents or diacritics. Pass in an `options` object like
650 * this to use a custom `transformString()` function:
651 * `{ transformString: s => latinize(s.toLocaleLowerCase()) }`
652 *
653 * @param {ScorerFunction} [options.scorer] An optional function that takes
654 * `string` and `query` parameters and returns a floating point number
655 * between 0 and 1 that represents how well the `query` matches the
656 * `string`. It defaults to the [quickScore()]{@link quickScore} function
657 * in this library.
658 *
659 * If the function gets a third `matches` parameter, it should fill the
660 * passed-in array with indexes corresponding to where the query
661 * matches the string, as described in the [search()]{@link QuickScore#search}
662 * method.
663 *
664 * @param {Config} [options.config] An optional object that is passed to
665 * the scorer function to further customize its behavior. If the
666 * `scorer` function has a `createConfig()` method on it, the `QuickScore`
667 * instance will call that with the `config` value and store the result.
668 * This can be used to extend the `config` parameter with default values.
669 */
670 function QuickScore() {
671 var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
672 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
673
674 _classCallCheck(this, QuickScore);
675
676 var _ref = Array.isArray(options) ? {
677 keys: options
678 } : options,
679 _ref$scorer = _ref.scorer,
680 scorer = _ref$scorer === void 0 ? quickScore : _ref$scorer,
681 _ref$transformString = _ref.transformString,
682 transformString = _ref$transformString === void 0 ? toLocaleLowerCase : _ref$transformString,
683 _ref$keys = _ref.keys,
684 keys = _ref$keys === void 0 ? [] : _ref$keys,
685 _ref$sortKey = _ref.sortKey,
686 sortKey = _ref$sortKey === void 0 ? "" : _ref$sortKey,
687 _ref$minimumScore = _ref.minimumScore,
688 minimumScore = _ref$minimumScore === void 0 ? 0 : _ref$minimumScore,
689 config = _ref.config;
690
691 this.scorer = scorer;
692 this.minimumScore = minimumScore;
693 this.config = config;
694 this.transformStringFunc = transformString;
695
696 if (typeof scorer.createConfig === "function") {
697 // let the scorer fill out the config with default values
698 this.config = scorer.createConfig(config);
699 }
700
701 this.setKeys(keys, sortKey);
702 this.setItems(items); // the scoring function needs access to this.sortKey
703
704 this.compareScoredStrings = this.compareScoredStrings.bind(this);
705 }
706 /**
707 * Scores the instance's items against the `query` and sorts them from
708 * highest to lowest.
709 *
710 * @param {string} query The string to score each item against. The
711 * instance's `transformString()` function is called on this string before
712 * it's matched against each item.
713 *
714 * @returns {Array<ScoredString|ScoredObject>} When the instance's `items`
715 * are flat strings, an array of [`ScoredString`]{@link ScoredString}
716 * objects containing the following properties is returned:
717 *
718 * - `item`: the string that was scored
719 * - `score`: the floating point score of the string for the current query
720 * - `matches`: an array of arrays that specify the character ranges
721 * where the query matched the string
722 *
723 * When the `items` are objects, an array of [`ScoredObject`]{@link ScoredObject}
724 * results is returned:
725 *
726 * - `item`: the object that was scored
727 * - `score`: the highest score from among the individual key scores
728 * - `scoreKey`: the name of the key with the highest score, which will be
729 * an empty string if they're all zero
730 * - `scoreValue`: the value of the key with the highest score, which makes
731 * it easier to access if it's a nested string
732 * - `scores`: a hash of the individual scores for each key
733 * - `matches`: a hash of arrays that specify the character ranges of the
734 * query match for each key
735 *
736 * The results array is sorted high to low on each item's score. Items with
737 * identical scores are sorted alphabetically and case-insensitively on the
738 * `sortKey` option. Items with scores that are <= the `minimumScore` option
739 * (defaults to `0`) are not returned, unless the `query` is falsy, in which
740 * case all of the items are returned, sorted alphabetically.
741 *
742 * The start and end indices in each [`RangeTuple`]{@link RangeTuple} in the
743 * `matches` array can be used as parameters to the `substring()` method to
744 * extract the characters from each string that match the query. This can
745 * then be used to format the matching characters with a different color or
746 * style.
747 *
748 * Each `ScoredObject` item also has a `_` property, which caches transformed
749 * versions of the item's strings, and might contain additional internal
750 * metadata in the future. It can be ignored.
751 */
752
753
754 _createClass(QuickScore, [{
755 key: "search",
756 value: function search(query) {
757 var results = [];
758 var items = this.items,
759 transformedItems = this.transformedItems,
760 sharedKeys = this.keys,
761 config = this.config; // if the query is empty, we want to return all items, so make the
762 // minimum score less than 0
763
764 var minScore = query ? this.minimumScore : -1;
765 var transformedQuery = this.transformString(query);
766 var itemCount = items.length;
767 var sharedKeyCount = sharedKeys.length;
768
769 if (typeof items[0] === "string") {
770 // items is an array of strings
771 for (var i = 0; i < itemCount; i++) {
772 var item = items[i];
773 var transformedItem = transformedItems[i];
774 var matches = [];
775 var score = this.scorer(item, query, matches, transformedItem, transformedQuery, config);
776
777 if (score > minScore) {
778 results.push({
779 item: item,
780 score: score,
781 matches: matches,
782 _: transformedItem
783 });
784 }
785 }
786 } else {
787 for (var _i = 0; _i < itemCount; _i++) {
788 var _item = items[_i];
789 var _transformedItem = transformedItems[_i];
790 var result = {
791 item: _item,
792 score: 0,
793 scoreKey: "",
794 scoreValue: "",
795 scores: {},
796 matches: {},
797 _: _transformedItem
798 }; // if an empty keys array was passed into the constructor,
799 // score all of the non-empty string keys on the object
800
801 var keys = sharedKeyCount ? sharedKeys : Object.keys(_transformedItem);
802 var keyCount = keys.length;
803 var highScore = 0;
804 var scoreKey = "";
805 var scoreValue = ""; // find the highest score for each keyed string on this item
806
807 for (var j = 0; j < keyCount; j++) {
808 var key = keys[j]; // use the key as the name if it's just a string, and
809 // default to the instance's scorer function
810
811 var _key$name = key.name,
812 name = _key$name === void 0 ? key : _key$name,
813 _key$scorer = key.scorer,
814 scorer = _key$scorer === void 0 ? this.scorer : _key$scorer;
815 var transformedString = _transformedItem[name]; // setItems() checks for non-strings and empty strings
816 // when creating the transformed objects, so if the key
817 // doesn't exist there, we can skip the processing
818 // below for this key in this item
819
820 if (transformedString) {
821 var string = this.getItemString(_item, key);
822 var _matches = [];
823 var newScore = scorer(string, query, _matches, transformedString, transformedQuery, config);
824 result.scores[name] = newScore;
825 result.matches[name] = _matches;
826
827 if (newScore > highScore) {
828 highScore = newScore;
829 scoreKey = name;
830 scoreValue = string;
831 }
832 }
833 }
834
835 if (highScore > minScore) {
836 result.score = highScore;
837 result.scoreKey = scoreKey;
838 result.scoreValue = scoreValue;
839 results.push(result);
840 }
841 }
842 }
843
844 results.sort(this.compareScoredStrings);
845 return results;
846 }
847 /**
848 * Sets the `keys` configuration. `setItems()` must be called after
849 * changing the keys so that the items' transformed strings get cached.
850 *
851 * @param {Array<ItemKey>} keys List of keys to score, as either strings
852 * or `{name, scorer}` objects.
853 *
854 * @param {string} [sortKey=keys[0]] Name of key on which to sort
855 * identically scored items. Defaults to the first `keys` item.
856 */
857
858 }, {
859 key: "setKeys",
860 value: function setKeys(keys, sortKey) {
861 // create a shallow copy of the keys array so that changes to its
862 // order outside of this instance won't affect searching
863 this.keys = keys.slice();
864 this.sortKey = sortKey;
865
866 if (this.keys.length) {
867 var scorer = this.scorer; // associate each key with the scorer function, if it isn't already
868
869 this.keys = this.keys.map(function (itemKey) {
870 // items in the keys array should either be a string or
871 // array specifying a key name, or a { name, scorer } object
872 var key = itemKey.length ? {
873 name: itemKey,
874 scorer: scorer
875 } : itemKey;
876
877 if (Array.isArray(key.name)) {
878 if (key.name.length > 1) {
879 key.path = key.name;
880 key.name = key.path.join(".");
881 } else {
882 // this path consists of just one key name, which was
883 // probably wrapped in an array because it contains
884 // dots but isn't intended as a key path. so don't
885 // create a path array on this key, so that we're not
886 // constantly calling reduce() to get this one key.
887 var _key$name2 = _slicedToArray(key.name, 1);
888
889 key.name = _key$name2[0];
890 }
891 } else if (key.name.indexOf(".") > -1) {
892 key.path = key.name.split(".");
893 }
894
895 return key;
896 });
897 this.sortKey = this.sortKey || this.keys[0].name;
898 }
899 }
900 /**
901 * Sets the `items` array and caches a transformed copy of all the item
902 * strings specified by the `keys` parameter to the constructor, using the
903 * `transformString` option (which defaults to `toLocaleLowerCase()`).
904 *
905 * @param {Array<string|object>} items List of items to score.
906 */
907
908 }, {
909 key: "setItems",
910 value: function setItems(items) {
911 // create a shallow copy of the items array so that changes to its
912 // order outside of this instance won't affect searching
913 var itemArray = items.slice();
914 var itemCount = itemArray.length;
915 var transformedItems = [];
916 var sharedKeys = this.keys;
917 var sharedKeyCount = sharedKeys.length;
918
919 if (typeof itemArray[0] === "string") {
920 for (var i = 0; i < itemCount; i++) {
921 transformedItems.push(this.transformString(itemArray[i]));
922 }
923 } else {
924 for (var _i2 = 0; _i2 < itemCount; _i2++) {
925 var item = itemArray[_i2];
926 var transformedItem = {};
927 var keys = sharedKeyCount ? sharedKeys : Object.keys(item);
928 var keyCount = keys.length;
929
930 for (var j = 0; j < keyCount; j++) {
931 var key = keys[j];
932 var string = this.getItemString(item, key);
933
934 if (string && typeof string === "string") {
935 transformedItem[key.name || key] = this.transformString(string);
936 }
937 }
938
939 transformedItems.push(transformedItem);
940 }
941 }
942
943 this.items = itemArray;
944 this.transformedItems = transformedItems;
945 }
946 /**
947 * Gets an item's key, possibly at a nested path.
948 *
949 * @private
950 * @param {object} item An object with multiple string properties.
951 * @param {object|string} key A key object with
952 * the name of the string to get from `item`, or a plain string when all
953 * keys on an item are being matched.
954 * @returns {string}
955 */
956
957 }, {
958 key: "getItemString",
959 value: function getItemString(item, key) {
960 var name = key.name,
961 path = key.path;
962
963 if (path) {
964 return path.reduce(function (value, prop) {
965 return value && value[prop];
966 }, item);
967 } else {
968 // if this instance is scoring all the keys on each item, key
969 // will just be a string, not a { name, scorer } object
970 return item[name || key];
971 }
972 }
973 /**
974 * Transforms a string into a canonical form for scoring.
975 *
976 * @private
977 * @param {string} string The string to transform.
978 * @returns {string}
979 */
980
981 }, {
982 key: "transformString",
983 value: function transformString(string) {
984 return this.transformStringFunc(string);
985 }
986 /**
987 * Compares two items based on their scores, or on their `sortKey` if the
988 * scores are identical.
989 *
990 * @private
991 * @param {object} a First item.
992 * @param {object} b Second item.
993 * @returns {number}
994 */
995
996 }, {
997 key: "compareScoredStrings",
998 value: function compareScoredStrings(a, b) {
999 // use the transformed versions of the strings for sorting
1000 var itemA = a._;
1001 var itemB = b._;
1002 var itemAString = typeof itemA === "string" ? itemA : itemA[this.sortKey];
1003 var itemBString = typeof itemB === "string" ? itemB : itemB[this.sortKey];
1004
1005 if (a.score === b.score) {
1006 // sort undefineds to the end of the array, as per the ES spec
1007 if (itemAString === undefined || itemBString === undefined) {
1008 if (itemAString === undefined && itemBString === undefined) {
1009 return 0;
1010 } else if (itemAString === undefined) {
1011 return 1;
1012 } else {
1013 return -1;
1014 }
1015 } else if (itemAString === itemBString) {
1016 return 0;
1017 } else if (itemAString < itemBString) {
1018 return -1;
1019 } else {
1020 return 1;
1021 }
1022 } else {
1023 return b.score - a.score;
1024 }
1025 }
1026 }]);
1027
1028 return QuickScore;
1029 }();
1030 /**
1031 * Default function for transforming each string to be searched.
1032 *
1033 * @private
1034 * @param {string} string The string to transform.
1035 * @returns {string} The transformed string.
1036 */
1037
1038 function toLocaleLowerCase(string) {
1039 return string.toLocaleLowerCase();
1040 }
1041
1042 exports.BaseConfig = BaseConfig;
1043 exports.DefaultConfig = DefaultConfig;
1044 exports.QuickScore = QuickScore;
1045 exports.QuicksilverConfig = QuicksilverConfig;
1046 exports.Range = Range;
1047 exports.createConfig = createConfig;
1048 exports.quickScore = quickScore;
1049
1050 Object.defineProperty(exports, '__esModule', { value: true });
1051
1052}));