|
'use strict'; |
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; /** |
|
* @name match-sorter |
|
* @license MIT license. |
|
* @copyright (c) 2016 Kent C. Dodds |
|
* @author Kent C. Dodds <kent@doddsfamily.us> |
|
*/ |
|
|
|
|
|
var _diacritic = require('diacritic'); |
|
|
|
var _diacritic2 = _interopRequireDefault(_diacritic); |
|
|
|
var _globalObject = require('global-object'); |
|
|
|
var _globalObject2 = _interopRequireDefault(_globalObject); |
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
|
|
|
var rankings = { |
|
EQUAL: 6, |
|
STARTS_WITH: 5, |
|
WORD_STARTS_WITH: 4, |
|
CONTAINS: 3, |
|
ACRONYM: 2, |
|
MATCHES: 1, |
|
NO_MATCH: 0 |
|
}; |
|
|
|
matchSorter.rankings = rankings; |
|
|
|
/** |
|
* Takes an array of items and a value and returns a new array with the items that match the given value |
|
* @param {Array} items - the items to sort |
|
* @param {String} value - the value to use for ranking |
|
* @param {Object} options - Some options to configure the sorter |
|
* @return {Array} - the new sorted array |
|
*/ |
|
function matchSorter(items, value) { |
|
var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; |
|
var keys = options.keys; |
|
var _options$threshold = options.threshold; |
|
var threshold = _options$threshold === undefined ? rankings.MATCHES : _options$threshold; |
|
|
|
var matchedItems = items.reduce(reduceItemsToRanked, []); |
|
return matchedItems.sort(sortRankedItems).map(function (_ref) { |
|
var item = _ref.item; |
|
return item; |
|
}); |
|
|
|
function reduceItemsToRanked(matches, item, index) { |
|
var _getHighestRanking = getHighestRanking(item, keys, value, options); |
|
|
|
var rank = _getHighestRanking.rank; |
|
var keyIndex = _getHighestRanking.keyIndex; |
|
|
|
if (rank >= threshold) { |
|
matches.push({ item: item, rank: rank, index: index, keyIndex: keyIndex }); |
|
} |
|
return matches; |
|
} |
|
} |
|
|
|
/** |
|
* Gets the highest ranking for value for the given item based on its values for the given keys |
|
* @param {*} item - the item to rank |
|
* @param {Array} keys - the keys to get values from the item for the ranking |
|
* @param {String} value - the value to rank against |
|
* @param {Object} options - options to control the ranking |
|
* @return {Number} - the highest ranking |
|
*/ |
|
function getHighestRanking(item, keys, value, options) { |
|
if (!keys) { |
|
return { rank: getMatchRanking(item, value, options), keyIndex: -1 }; |
|
} |
|
var valuesToRank = getAllValuesToRank(item, keys); |
|
return valuesToRank.reduce(function (_ref2, itemValue, i) { |
|
var rank = _ref2.rank; |
|
var keyIndex = _ref2.keyIndex; |
|
|
|
var newRank = getMatchRanking(itemValue, value, options); |
|
if (newRank > rank) { |
|
rank = newRank; |
|
keyIndex = i; |
|
} |
|
return { rank: rank, keyIndex: keyIndex }; |
|
}, { rank: rankings.NO_MATCH, keyIndex: -1 }); |
|
} |
|
|
|
/** |
|
* Gives a rankings score based on how well the two strings match. |
|
* @param {String} testString - the string to test against |
|
* @param {String} stringToRank - the string to rank |
|
* @param {Object} options - options for the match (like keepDiacritics for comparison) |
|
* @returns {Number} the ranking for how well stringToRank matches testString |
|
*/ |
|
function getMatchRanking(testString, stringToRank, options) { |
|
/* eslint complexity:[2, 8] */ |
|
testString = prepareValueForComparison(testString, options); |
|
stringToRank = prepareValueForComparison(stringToRank, options); |
|
|
|
// too long |
|
if (stringToRank.length > testString.length) { |
|
return rankings.NO_MATCH; |
|
} |
|
|
|
// equals |
|
if (testString === stringToRank) { |
|
return rankings.EQUAL; |
|
} |
|
|
|
// starts with |
|
if (testString.indexOf(stringToRank) === 0) { |
|
return rankings.STARTS_WITH; |
|
} |
|
|
|
// word starts with |
|
if (testString.indexOf(' ' + stringToRank) !== -1) { |
|
return rankings.WORD_STARTS_WITH; |
|
} |
|
|
|
// contains |
|
if (testString.indexOf(stringToRank) !== -1) { |
|
return rankings.CONTAINS; |
|
} else if (stringToRank.length === 1) { |
|
// If the only character in the given stringToRank |
|
// isn't even contained in the testString, then |
|
// it's definitely not a match. |
|
return rankings.NO_MATCH; |
|
} |
|
|
|
// acronym |
|
if (getAcronym(testString).indexOf(stringToRank) !== -1) { |
|
return rankings.ACRONYM; |
|
} |
|
|
|
return stringsByCharOrder(testString, stringToRank); |
|
} |
|
|
|
/** |
|
* Generates an acronym for a string. |
|
* |
|
* @param {String} string the string for which to produce the acronym |
|
* @returns {String} the acronym |
|
*/ |
|
function getAcronym(string) { |
|
var acronym = ''; |
|
var wordsInString = string.split(' '); |
|
wordsInString.forEach(function (wordInString) { |
|
var splitByHyphenWords = wordInString.split('-'); |
|
splitByHyphenWords.forEach(function (splitByHyphenWord) { |
|
acronym += splitByHyphenWord.substr(0, 1); |
|
}); |
|
}); |
|
return acronym; |
|
} |
|
|
|
/** |
|
* Returns a rankings.matches or noMatch score based on whether |
|
* the characters in the stringToRank are found in order in the |
|
* testString |
|
* @param {String} testString - the string to test against |
|
* @param {String} stringToRank - the string to rank |
|
* @returns {Number} the ranking for how well stringToRank matches testString |
|
*/ |
|
function stringsByCharOrder(testString, stringToRank) { |
|
var charNumber = 0; |
|
|
|
function findMatchingCharacter(matchChar, string) { |
|
var found = false; |
|
for (var j = charNumber; j < string.length; j++) { |
|
var stringChar = string[j]; |
|
if (stringChar === matchChar) { |
|
found = true; |
|
charNumber = j + 1; |
|
break; |
|
} |
|
} |
|
return found; |
|
} |
|
|
|
for (var i = 0; i < stringToRank.length; i++) { |
|
var matchChar = stringToRank[i]; |
|
var found = findMatchingCharacter(matchChar, testString); |
|
if (!found) { |
|
return rankings.NO_MATCH; |
|
} |
|
} |
|
return rankings.MATCHES; |
|
} |
|
|
|
/** |
|
* Sorts items that have a rank, index, and keyIndex |
|
* @param {Object} a - the first item to sort |
|
* @param {Object} b - the second item to sort |
|
* @return {Number} -1 if a should come first, 1 if b should come first |
|
* Note: will never return 0 |
|
*/ |
|
function sortRankedItems(a, b) { |
|
var aFirst = -1; |
|
var bFirst = 1; |
|
var aRank = a.rank; |
|
var aIndex = a.index; |
|
var aKeyIndex = a.keyIndex; |
|
var bRank = b.rank; |
|
var bIndex = b.index; |
|
var bKeyIndex = b.keyIndex; |
|
|
|
var same = aRank === bRank; |
|
if (same) { |
|
if (aKeyIndex === bKeyIndex) { |
|
return aIndex < bIndex ? aFirst : bFirst; |
|
} else { |
|
return aKeyIndex < bKeyIndex ? aFirst : bFirst; |
|
} |
|
} else { |
|
return aRank > bRank ? aFirst : bFirst; |
|
} |
|
} |
|
|
|
/** |
|
* Prepares value for comparison by stringifying it, removing diacritics (if specified), and toLowerCase-ing it |
|
* @param {String} value - the value to clean |
|
* @param {Object} options - {keepDiacritics: whether to remove diacritics} |
|
* @return {String} the prepared value |
|
*/ |
|
function prepareValueForComparison(value, _ref3) { |
|
var keepDiacritics = _ref3.keepDiacritics; |
|
|
|
value = '' + value; // toString |
|
if (!keepDiacritics) { |
|
value = _diacritic2.default.clean(value); |
|
} |
|
return value.toLowerCase(); |
|
} |
|
|
|
/** |
|
* Gets value for key in item at arbitrarily nested keypath |
|
* @param {Object} item - the item |
|
* @param {Object|Function} key - the potentially nested keypath or property callback |
|
* @return {String} - the value at nested keypath |
|
*/ |
|
function getItemValue(item, key) { |
|
if (typeof key === 'function') { |
|
return key(item); |
|
} |
|
var isNested = key.indexOf('.') !== -1; |
|
if (!isNested) { |
|
return item[key]; |
|
} |
|
return key.split('.').reduce(function (itemObj, nestedKey) { |
|
return itemObj[nestedKey]; |
|
}, item); |
|
} |
|
|
|
/** |
|
* Gets all the values for the given keys in the given item and returns an array of those values |
|
* @param {Object} item - the item from which the values will be retrieved |
|
* @param {Array} keys - the keys to use to retrieve the values |
|
* @return {Array} the values in an array |
|
*/ |
|
function getAllValuesToRank(item, keys) { |
|
return keys.reduce(function (allVals, key) { |
|
return allVals.concat(getItemValue(item, key)); |
|
}, []); |
|
} |
|
|
|
// some manual ✨ magic umd ✨ here because Rollup isn't capable of exposing our module the way we want |
|
// see dist-test/index.js |
|
/* istanbul ignore next */ |
|
if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object' && typeof module !== 'undefined') { |
|
matchSorter.default = matchSorter; |
|
module.exports = matchSorter; |
|
Object.defineProperty(exports, '__esModule', { value: true }); |
|
} else if (typeof define === 'function' && define.amd) { |
|
// eslint-disable-line |
|
define(function () { |
|
return matchSorter; |
|
}); // eslint-disable-line |
|
} else { |
|
_globalObject2.default.matchSorter = matchSorter; // eslint-disable-line |
|
} |
I noticed that there are a few statements in the slice that don't really need to be (like
b.index;
). Like I said, it's definitely still a work in progress!