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 | }));
|