/*! sifter.js | https://github.com/orchidjs/sifter.js | Apache License (v2) */
(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
	typeof define === 'function' && define.amd ? define(factory) :
	(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.sifter = factory());
}(this, (function () { 'use strict';

	// @ts-ignore TS2691 "An import path cannot end with a '.ts' extension"
	// https://github.com/andrewrk/node-diacritics/blob/master/index.js
	var latin_pat;
	const accent_pat = '[\u0300-\u036F\u{b7}\u{2be}]'; // \u{2bc}

	const accent_reg = new RegExp(accent_pat, 'gu');
	var diacritic_patterns;
	const latin_convert = {
	  'æ': 'ae',
	  'ⱥ': 'a',
	  'ø': 'o'
	};
	const convert_pat = new RegExp(Object.keys(latin_convert).join('|'), 'gu');
	const code_points = [[0, 65535]];
	/**
	 * Remove accents
	 * via https://github.com/krisk/Fuse/issues/133#issuecomment-318692703
	 *
	 */

	const asciifold = str => {
	  return str.normalize('NFKD').replace(accent_reg, '').toLowerCase().replace(convert_pat, function (foreignletter) {
	    return latin_convert[foreignletter] || foreignletter;
	  });
	};
	/**
	 * Convert array of strings to a regular expression
	 *	ex ['ab','a'] => (?:ab|a)
	 * 	ex ['a','b'] => [ab]
	 *
	 */

	const arrayToPattern = (chars, glue = '|') => {
	  if (chars.length === 1 && chars[0] != undefined) {
	    return chars[0];
	  }

	  var longest = 1;
	  chars.forEach(a => {
	    longest = Math.max(longest, a.length);
	  });

	  if (longest == 1) {
	    return '[' + chars.join('') + ']';
	  }

	  return '(?:' + chars.join(glue) + ')';
	};
	const escapeToPattern = chars => {
	  const escaped = chars.map(diacritic => escape_regex(diacritic));
	  return arrayToPattern(escaped);
	};
	/**
	 * Get all possible combinations of substrings that add up to the given string
	 * https://stackoverflow.com/questions/30169587/find-all-the-combination-of-substrings-that-add-up-to-the-given-string
	 *
	 */

	const allSubstrings = input => {
	  if (input.length === 1) return [[input]];
	  var result = [];
	  allSubstrings(input.substring(1)).forEach(function (subresult) {
	    var tmp = subresult.slice(0);
	    tmp[0] = input.charAt(0) + tmp[0];
	    result.push(tmp);
	    tmp = subresult.slice(0);
	    tmp.unshift(input.charAt(0));
	    result.push(tmp);
	  });
	  return result;
	};
	/**
	 * Generate a list of diacritics from the list of code points
	 *
	 */

	const generateDiacritics = code_points => {
	  var diacritics = {};
	  code_points.forEach(code_range => {
	    for (let i = code_range[0]; i <= code_range[1]; i++) {
	      let diacritic = String.fromCharCode(i);
	      let latin = asciifold(diacritic);

	      if (latin == diacritic.toLowerCase()) {
	        continue;
	      } // skip when latin is a string longer than 3 characters long
	      // bc the resulting regex patterns will be long
	      // eg:
	      // latin صلى الله عليه وسلم length 18 code point 65018
	      // latin جل جلاله length 8 code point 65019


	      if (latin.length > 3) {
	        continue;
	      }

	      const latin_diacritics = diacritics[latin] || [latin];
	      const patt = new RegExp(escapeToPattern(latin_diacritics), 'iu');

	      if (diacritic.match(patt)) {
	        continue;
	      }

	      latin_diacritics.push(diacritic);
	      diacritics[latin] = latin_diacritics;
	    }
	  }); // filter out if there's only one character in the list
	  // todo: this may not be needed

	  Object.keys(diacritics).forEach(latin => {
	    const latin_diacritics = diacritics[latin] || [];

	    if (latin_diacritics.length < 2) {
	      delete diacritics[latin];
	    }
	  }); // latin character pattern
	  // match longer substrings first

	  let latin_chars = Object.keys(diacritics).sort((a, b) => b.length - a.length);
	  latin_pat = new RegExp('(' + escapeToPattern(latin_chars) + accent_pat + '*)', 'gu'); // build diacritic patterns
	  // ae needs:
	  //	(?:(?:ae|Æ|Ǽ|Ǣ)|(?:A|Ⓐ|Ａ...)(?:E|ɛ|Ⓔ...))

	  var diacritic_patterns = {};
	  latin_chars.sort((a, b) => a.length - b.length).forEach(latin => {
	    var substrings = allSubstrings(latin);
	    var pattern = substrings.map(sub_pat => {
	      sub_pat = sub_pat.map(l => {
	        if (diacritics.hasOwnProperty(l)) {
	          return escapeToPattern(diacritics[l]);
	        }

	        return l;
	      });
	      return arrayToPattern(sub_pat, '');
	    });
	    diacritic_patterns[latin] = arrayToPattern(pattern);
	  });
	  return diacritic_patterns;
	};
	/**
	 * Expand a regular expression pattern to include diacritics
	 * 	eg /a/ becomes /aⓐａẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐɑAⒶＡÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ/
	 *
	 */

	const diacriticRegexPoints = regex => {
	  if (diacritic_patterns === undefined) {
	    diacritic_patterns = generateDiacritics(code_points);
	  }

	  const decomposed = regex.normalize('NFKD').toLowerCase();
	  return decomposed.split(latin_pat).map(part => {
	    // "ﬄ" or "ffl"
	    const no_accent = asciifold(part);

	    if (no_accent == '') {
	      return '';
	    }

	    if (diacritic_patterns.hasOwnProperty(no_accent)) {
	      return diacritic_patterns[no_accent];
	    }

	    return part;
	  }).join('');
	};

	// @ts-ignore TS2691 "An import path cannot end with a '.ts' extension"

	/**
	 * A property getter resolving dot-notation
	 * @param  {Object}  obj     The root object to fetch property on
	 * @param  {String}  name    The optionally dotted property name to fetch
	 * @return {Object}          The resolved property value
	 */
	const getAttr = (obj, name) => {
	  if (!obj) return;
	  return obj[name];
	};
	/**
	 * A property getter resolving dot-notation
	 * @param  {Object}  obj     The root object to fetch property on
	 * @param  {String}  name    The optionally dotted property name to fetch
	 * @return {Object}          The resolved property value
	 */

	const getAttrNesting = (obj, name) => {
	  if (!obj) return;
	  var part,
	      names = name.split(".");

	  while ((part = names.shift()) && (obj = obj[part]));

	  return obj;
	};
	/**
	 * Calculates how close of a match the
	 * given value is against a search token.
	 *
	 */

	const scoreValue = (value, token, weight) => {
	  var score, pos;
	  if (!value) return 0;
	  value = value + '';
	  pos = value.search(token.regex);
	  if (pos === -1) return 0;
	  score = token.string.length / value.length;
	  if (pos === 0) score += 0.5;
	  return score * weight;
	};
	/**
	 *
	 * https://stackoverflow.com/questions/63006601/why-does-u-throw-an-invalid-escape-error
	 */

	const escape_regex = str => {
	  return (str + '').replace(/([\$\(-\+\.\?\[-\^\{-\}])/g, '\\$1');
	};
	/**
	 * Cast object property to an array if it exists and has a value
	 *
	 */

	const propToArray = (obj, key) => {
	  var value = obj[key];
	  if (typeof value == 'function') return value;

	  if (value && !Array.isArray(value)) {
	    obj[key] = [value];
	  }
	};
	/**
	 * Iterates over arrays and hashes.
	 *
	 * ```
	 * iterate(this.items, function(item, id) {
	 *    // invoked for each item
	 * });
	 * ```
	 *
	 */

	const iterate = (object, callback) => {
	  if (Array.isArray(object)) {
	    object.forEach(callback);
	  } else {
	    for (var key in object) {
	      if (object.hasOwnProperty(key)) {
	        callback(object[key], key);
	      }
	    }
	  }
	};
	const cmp = (a, b) => {
	  if (typeof a === 'number' && typeof b === 'number') {
	    return a > b ? 1 : a < b ? -1 : 0;
	  }

	  a = asciifold(a + '').toLowerCase();
	  b = asciifold(b + '').toLowerCase();
	  if (a > b) return 1;
	  if (b > a) return -1;
	  return 0;
	};

	/**
	 * sifter.js
	 * Copyright (c) 2013–2020 Brian Reavis & contributors
	 *
	 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
	 * file except in compliance with the License. You may obtain a copy of the License at:
	 * http://www.apache.org/licenses/LICENSE-2.0
	 *
	 * Unless required by applicable law or agreed to in writing, software distributed under
	 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
	 * ANY KIND, either express or implied. See the License for the specific language
	 * governing permissions and limitations under the License.
	 *
	 * @author Brian Reavis <brian@thirdroute.com>
	 */

	class Sifter {
	  // []|{};

	  /**
	   * Textually searches arrays and hashes of objects
	   * by property (or multiple properties). Designed
	   * specifically for autocomplete.
	   *
	   */
	  constructor(items, settings) {
	    this.items = void 0;
	    this.settings = void 0;
	    this.items = items;
	    this.settings = settings || {
	      diacritics: true
	    };
	  }

	  /**
	   * Splits a search string into an array of individual
	   * regexps to be used to match results.
	   *
	   */
	  tokenize(query, respect_word_boundaries, weights) {
	    if (!query || !query.length) return [];
	    const tokens = [];
	    const words = query.split(/\s+/);
	    var field_regex;

	    if (weights) {
	      field_regex = new RegExp('^(' + Object.keys(weights).map(escape_regex).join('|') + ')\:(.*)$');
	    }

	    words.forEach(word => {
	      let field_match;
	      let field = null;
	      let regex = null; // look for "field:query" tokens

	      if (field_regex && (field_match = word.match(field_regex))) {
	        field = field_match[1];
	        word = field_match[2];
	      }

	      if (word.length > 0) {
	        if (this.settings.diacritics) {
	          regex = diacriticRegexPoints(word);
	        } else {
	          regex = escape_regex(word);
	        }

	        if (respect_word_boundaries) regex = "\\b" + regex;
	      }

	      tokens.push({
	        string: word,
	        regex: regex ? new RegExp(regex, 'iu') : null,
	        field: field
	      });
	    });
	    return tokens;
	  }

	  /**
	   * Returns a function to be used to score individual results.
	   *
	   * Good matches will have a higher score than poor matches.
	   * If an item is not a match, 0 will be returned by the function.
	   *
	   * @returns {function}
	   */
	  getScoreFunction(query, options) {
	    var search = this.prepareSearch(query, options);
	    return this._getScoreFunction(search);
	  }

	  _getScoreFunction(search) {
	    const tokens = search.tokens,
	          token_count = tokens.length;

	    if (!token_count) {
	      return function () {
	        return 0;
	      };
	    }

	    const fields = search.options.fields,
	          weights = search.weights,
	          field_count = fields.length,
	          getAttrFn = search.getAttrFn;

	    if (!field_count) {
	      return function () {
	        return 1;
	      };
	    }
	    /**
	     * Calculates the score of an object
	     * against the search query.
	     *
	     */


	    const scoreObject = function () {
	      if (field_count === 1) {
	        return function (token, data) {
	          const field = fields[0].field;
	          return scoreValue(getAttrFn(data, field), token, weights[field]);
	        };
	      }

	      return function (token, data) {
	        var sum = 0; // is the token specific to a field?

	        if (token.field) {
	          const value = getAttrFn(data, token.field);

	          if (!token.regex && value) {
	            sum += 1 / field_count;
	          } else {
	            sum += scoreValue(value, token, 1);
	          }
	        } else {
	          iterate(weights, (weight, field) => {
	            sum += scoreValue(getAttrFn(data, field), token, weight);
	          });
	        }

	        return sum / field_count;
	      };
	    }();

	    if (token_count === 1) {
	      return function (data) {
	        return scoreObject(tokens[0], data);
	      };
	    }

	    if (search.options.conjunction === 'and') {
	      return function (data) {
	        var i = 0,
	            score,
	            sum = 0;

	        for (; i < token_count; i++) {
	          score = scoreObject(tokens[i], data);
	          if (score <= 0) return 0;
	          sum += score;
	        }

	        return sum / token_count;
	      };
	    } else {
	      return function (data) {
	        var sum = 0;
	        iterate(tokens, token => {
	          sum += scoreObject(token, data);
	        });
	        return sum / token_count;
	      };
	    }
	  }

	  /**
	   * Returns a function that can be used to compare two
	   * results, for sorting purposes. If no sorting should
	   * be performed, `null` will be returned.
	   *
	   * @return function(a,b)
	   */
	  getSortFunction(query, options) {
	    var search = this.prepareSearch(query, options);
	    return this._getSortFunction(search);
	  }

	  _getSortFunction(search) {
	    var i, n, implicit_score;
	    const self = this,
	          options = search.options,
	          sort = !search.query && options.sort_empty ? options.sort_empty : options.sort,
	          sort_flds = [],
	          multipliers = [];

	    if (typeof sort == 'function') {
	      return sort.bind(this);
	    }
	    /**
	     * Fetches the specified sort field value
	     * from a search result item.
	     *
	     */


	    const get_field = function get_field(name, result) {
	      if (name === '$score') return result.score;
	      return search.getAttrFn(self.items[result.id], name);
	    }; // parse options


	    if (sort) {
	      for (i = 0, n = sort.length; i < n; i++) {
	        if (search.query || sort[i].field !== '$score') {
	          sort_flds.push(sort[i]);
	        }
	      }
	    } // the "$score" field is implied to be the primary
	    // sort field, unless it's manually specified


	    if (search.query) {
	      implicit_score = true;

	      for (i = 0, n = sort_flds.length; i < n; i++) {
	        if (sort_flds[i].field === '$score') {
	          implicit_score = false;
	          break;
	        }
	      }

	      if (implicit_score) {
	        sort_flds.unshift({
	          field: '$score',
	          direction: 'desc'
	        });
	      }
	    } else {
	      for (i = 0, n = sort_flds.length; i < n; i++) {
	        if (sort_flds[i].field === '$score') {
	          sort_flds.splice(i, 1);
	          break;
	        }
	      }
	    } // build function


	    const sort_flds_count = sort_flds.length;

	    if (!sort_flds_count) {
	      return null;
	    }

	    return function (a, b) {
	      var i, result, field;

	      for (i = 0; i < sort_flds_count; i++) {
	        field = sort_flds[i].field;
	        let multiplier = multipliers[i];

	        if (multiplier == undefined) {
	          multiplier = sort_flds[i].direction === 'desc' ? -1 : 1;
	        }

	        result = multiplier * cmp(get_field(field, a), get_field(field, b));
	        if (result) return result;
	      }

	      return 0;
	    };
	  }

	  /**
	   * Parses a search query and returns an object
	   * with tokens and fields ready to be populated
	   * with results.
	   *
	   */
	  prepareSearch(query, optsUser) {
	    const weights = {};
	    var options = Object.assign({}, optsUser);
	    propToArray(options, 'sort');
	    propToArray(options, 'sort_empty'); // convert fields to new format

	    if (options.fields) {
	      propToArray(options, 'fields');
	      const fields = [];
	      options.fields.forEach(field => {
	        if (typeof field == 'string') {
	          field = {
	            field: field,
	            weight: 1
	          };
	        }

	        fields.push(field);
	        weights[field.field] = 'weight' in field ? field.weight : 1;
	      });
	      options.fields = fields;
	    }

	    return {
	      options: options,
	      query: query.toLowerCase().trim(),
	      tokens: this.tokenize(query, options.respect_word_boundaries, weights),
	      total: 0,
	      items: [],
	      weights: weights,
	      getAttrFn: options.nesting ? getAttrNesting : getAttr
	    };
	  }

	  /**
	   * Searches through all items and returns a sorted array of matches.
	   *
	   */
	  search(query, options) {
	    var self = this,
	        score,
	        search;
	    search = this.prepareSearch(query, options);
	    options = search.options;
	    query = search.query; // generate result scoring function

	    const fn_score = options.score || self._getScoreFunction(search); // perform search and sort


	    if (query.length) {
	      iterate(self.items, (item, id) => {
	        score = fn_score(item);

	        if (options.filter === false || score > 0) {
	          search.items.push({
	            'score': score,
	            'id': id
	          });
	        }
	      });
	    } else {
	      iterate(self.items, (_, id) => {
	        search.items.push({
	          'score': 1,
	          'id': id
	        });
	      });
	    }

	    const fn_sort = self._getSortFunction(search);

	    if (fn_sort) search.items.sort(fn_sort); // apply limits

	    search.total = search.items.length;

	    if (typeof options.limit === 'number') {
	      search.items = search.items.slice(0, options.limit);
	    }

	    return search;
	  }

	}

	return Sifter;

})));
//# sourceMappingURL=sifter.js.map
