/**
 *
 * handlebar-kit
 *
 * Copyright (c) 2020, Jocelyn Badgley
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Portions of the MIT licensed date-fns library are bundled with this
 * software. https://github.com/date-fns/date-fns#readme
 */
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

function equals (value) {
  value = uc(value);
  return (tok) => uc(tok) === value;
}

function re (pattern) {
  if (isString(pattern)) pattern = new RegExp(pattern);
  return (tok) => !!String(tok).match(pattern);
}

function anyOf (...args) {
  args = args.flat().map(uc);
  if (!anyBy(args, isFunction)) {
    // arguments do not contain a function, so we can optimize
    if (args.length === 1) return (tok) => uc(tok) === args[0];
    return (tok) => args.includes(uc(tok));
  }

  args = args.map((a) => isFunction(a) && a || equals(a));
  if (args.length === 1) return (tok) => args[0](tok);
  return (tok) => anyBy(args, (check) => check(tok));
}

function allOf (...args) {
  args = args.flat().map((a) => isFunction(a) && a || equals(a));
  if (args.length === 1) return (tok) => args[0](tok);
  return (tok) => allBy(args, (check) => check(tok));
}

function isNumber    (input) { return typeof input === 'number' && !Number.isNaN(input); }
function isString    (input) { return typeof input === 'string'; }
function isBoolean   (input) { return typeof input === 'boolean'; }
function isFunction  (input) { return typeof input === 'function'; }
function isUndefined (input) { return typeof input === 'undefined'; }
function isMap       (input) { return input instanceof Map; }
function isSet       (input) { return input instanceof Set; }
function isDate      (input) { return input instanceof Date; }
function isRegExp    (input) { return input instanceof RegExp; }
function isTruthy    (input) { return !!input; }
function isFalsey    (input) { return  !input; }
function isNull      (input) { return input === null; }
const isArray = Array.isArray;

function isPrimitive (input) {
  switch (typeof input) {
  case 'string':
  case 'number':
  case 'boolean':
    return true;
  default:
    return false;
  }
}

function isObject (input) {
  if (!input) return false;
  if (typeof input !== 'object') return false;
  if (isArray(input)) return false;
  if (!(input instanceof Object)) return false;
  if (input.constructor !== Object.prototype.constructor) return false;
  return true;
}


const IS_LOOKUP = new Map([
  [ Array,     isArray     ],
  [ Number,    isNumber    ],
  [ String,    isString    ],
  [ Boolean,   isBoolean   ],
  [ Map,       isMap       ],
  [ Set,       isSet       ],
  [ Function,  isFunction  ],
  [ Date,      isDate      ],
  [ undefined, isUndefined ],
  [ true,      isTruthy    ],
  [ false,     isFalsey    ],
]);

function is (...args) {
  args = args.flat().map((a) =>
    IS_LOOKUP.get(a)
		|| (isFunction(a) && a)
		|| (isRegExp(a) && re(a))
		|| equals(a)
  );
  if (args.length === 1) return (tok) => args[0](tok);
  return (tok) => anyBy(args, (check) => check(tok));
}

function isAll (...args) {
  args = args.flat().map((a) =>
    IS_LOOKUP.get(a)
		|| (isFunction(a) && a)
		|| (isRegExp(a) && re(a))
		|| equals(a)
  );
  if (args.length === 1) return (tok) => args[0](tok);
  return (tok) => allBy(args, (check) => check(tok));
}

function isArrayOf (...args) {
  const predicate = is(...args);
  return (tok) => (isArray(tok) ? allBy(tok, predicate) : predicate(tok));
}
function isArrayOfStrings    (input) { return allBy(input, isString); }
function isArrayOfNumbers    (input) { return allBy(input, isNumber); }
function isArrayOfBooleans   (input) { return allBy(input, isBoolean); }
function isArrayOfObjects    (input) { return allBy(input, isObject); }
function isArrayOfMappables  (input) { return allBy(input, isMappable); }
function isArrayOfPrimatives (input) { return allBy(input, isPrimitive); }
function isArrayOfFunctions  (input) { return allBy(input, isFunction); }
function isArrayOfRegEx      (input) { return allBy(input, isRegExp); }
function isArrayOfTruthy     (input) { return allBy(input, isTruthy); }
function isArrayOfFalsey     (input) { return allBy(input, isFalsey); }

function contains (...args) {
  const predicate = is(...args);
  return (tok) => (isArray(tok) ? anyBy(tok, predicate) : predicate(tok));
}
function containsStrings    (input) { return anyBy(input, isString); }
function containsNumbers    (input) { return anyBy(input, isNumber); }
function containsBooleans   (input) { return anyBy(input, isBoolean); }
function containsObjects    (input) { return anyBy(input, isObject); }
function containsMappables  (input) { return anyBy(input, isMappable); }
function containsPrimatives (input) { return anyBy(input, isPrimitive); }
function containsFunctions  (input) { return anyBy(input, isFunction); }
function containsRegEx      (input) { return anyBy(input, isRegExp); }
function containsTruthy     (input) { return anyBy(input, isTruthy); }
function containsFalsey     (input) { return anyBy(input, isFalsey); }

function truthy (value) {
  if (isMappable(value)) return !!sizeOf(value);
  return !!value;
}

function hasOwn (obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

function lc (str) {
  return isString(uc) ? str.toLowerCase() : str;
}

function uc (str) {
  return isString(str) ? str.toUpperCase() : str;
}

function ucfirst (input) {
  input = String(input);
  return input.charAt(0).toUpperCase() + input.slice(1);
}

function ucsentence (input) {
  return input.replace(/((?:\S[^.?!]*)[.?!]*)/g, (txt) =>
    txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
  );
}

function ucwords (input) {
  return input.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.substr(1));
}

function merge (...sources) {
  const result = {};
  for (const source of sources) {
    if (!source) continue;
    for (const [ key, value ] of Object.entries(source)) {
      if (isObject(value)) {
        if (isObject(result[key])) {
          result[key] = merge(result[key], value);
        } else {
          result[key] = merge(value);
        }
      } else {
        result[key] = value;
      }
    }
  }
  return result;
}

function set (obj, path, value) {
  if (path === null && path === undefined && path === '') return false;
  if (isNumber(path)) path = [ String(path) ];
  else if (isString(path)) {
    if (hasOwn(obj, path)) {
      obj[path] = value;
      return obj;
    }
    path = path.split(/[,[\].]+?/);
  }

  const c = path.length - 1;
  path
    .filter((s) => s || s === 0)
    .reduce((res, key, i) => {
      if (i === c) {
        res[key] = value;
        return true;
      }
      if (isObject(res[key]) || isFunction(res[key])) return res[key];
      return (res[key] = {});
    }, obj);

  return obj;
}

function get (obj, path, defaultValue) {
  if (path === null && path === undefined && path === '') return defaultValue;
  if (isNumber(path)) path = [ String(path) ];
  else if (isString(path)) {
    if (hasOwn(obj, path)) return obj[path];
    path = path.split(/[,[\].]+?/);
  }

  const result = path
    .filter((s) => s !== null && s !== undefined && s !== '')
    .reduce((res, key) =>
      ((res !== null && res !== undefined) ? res[key] : res)
    , obj);
  return (result === undefined || result === obj) ? defaultValue : result;
}

function has (obj, path) {
  if (isNumber(path)) path = [ String(path) ];
  else if (isString(path)) path = String.prototype.split.call(path, /[,[\].]+?/);
  let res = obj;
  for (const key of path) {
    if (res === null || res === undefined) return false;
    if (typeof res !== 'object' && typeof res !== 'function') return false;
    if (!hasOwn(res, key)) return false;
    res = res[key];
  }
  return true;
}

function isMappable (collection, arrays = true) {
  return (
    (arrays && isArray(collection)) ||
		(arrays && isSet(collection)) ||
		isMap(collection) ||
		collection && (typeof collection === 'object' || typeof collection === 'function')
  );
}

function sizeOf (collection) {
  if (isArray(collection) || isString(collection)) return collection.length;
  if (isSet(collection) || isMap(collection)) return collection.size;
  if (isObject(collection)) return Object.keys(collection).length;
  return !!collection;
}

function keys (input) {
  if (isArray(input)) return [ ...input.keys() ];

  if (isSet(input)) return Array.from(input.entries(), ([ k ]) => k);

  if (isMap(input)) return Array.from(input.keys());

  if (isObject(input)) return Object.keys(input);

  return [];
}

function values (input) {
  if (isArray(input)) return [ ...input ];

  if (isSet(input) || isMap(input)) return Array.from(input.values());

  if (isObject(input)) return Object.values(input);

  return [];
}

function arrayify (input) {
  if (isArray(input)) return input;

  if (isSet(input) || isMap(input)) return Array.from(input.values());

  if (isObject(input)) return Object.values(input);

  return [ input ];
}

function first (input, count = 1) {
  if (count === 1) {
    if (isArray(input) || isString(input)) return input[0];
    if (isSet(input) || isObject(input)) for (const v of input) return v;
    if (isMap(input)) for (const [ , v ] of input) return v;
    return;
  }

  if (isArray(input) || isString(input)) return input.slice(0, count);
  if (isSet(input)) return Array.from(input).slice(0, count);
  if (isObject(input)) return Object.values(input).slice(0, count);
  if (isMap(input)) return Array.from(input.values()).slice(0, count);
}

function last (input, count = 1) {
  if (count === 1) {
    if (isArray(input) || isString(input)) return input[input.length - 1];
  }

  if (isArray(input) || isString(input)) return input.slice(-count);
  if (isSet(input)) return Array.from(input).slice(-count);
  if (isObject(input)) return Object.values(input).slice(-count);
  if (isMap(input)) return Array.from(input.values()).slice(-count);
}

function all (...args) {
  let input;
  if (args.length > 1) {
    input = args;
  } else {
    input = arrayify(args[0]);
  }

  let result = input.shift();
  for (const value of input) {
    if (!truthy(result)) {
      return false;
    }
    result = value;
  }

  return result;
}

function allBy (collection, predicate = null) {
  if (!collection) return false;
  if (predicate === null) {
    predicate = (v) => v;
  } else if (!isFunction(predicate)) {
    predicate = iteratee(predicate);
  }

  if (isArray(collection)) {
    let i = 0;
    for (const value of collection) {
      if (!predicate(value, i, i++)) return false;
    }
    return true;
  }

  if (isSet(collection)) {
    let i = 0;
    for (const item of collection) {
      if (!predicate(item, i, i++)) return false;
    }
    return true;
  }

  // received a Map
  if (isMap(collection)) {
    let i = 0;
    for (const [ key, value ] of collection.entries()) {
      if (!predicate(value, key, i++)) return false;
    }
    return true;
  }

  // received an object hash
  if (isObject(collection)) {
    let i = 0;
    for (const [ key, value ] of Object.entries(collection)) {
      if (!predicate(value, key, i++)) return false;
    }
    return true;
  }

  return !!collection;
}

function any (...args) {
  let input;
  if (args.length > 1) {
    input = args;
  } else {
    input = arrayify(args[0]);
  }

  for (const value of input) {
    if (truthy(value)) {
      return value;
    }
  }

  return input[input.length - 1];
}

function anyBy (collection, predicate = null) {
  if (!collection) return false;
  if (predicate === null) {
    predicate = (v) => v;
  } else if (!isFunction(iteratee)) {
    predicate = iteratee(predicate);
  }

  if (isArray(collection)) {
    let i = 0;
    for (const value of collection) {
      if (predicate(value, i, i++)) return true;
    }
    return false;
  }

  if (isSet(collection)) {
    let i = 0;
    for (const item of collection) {
      if (predicate(item, i, i++)) return true;
    }
    return false;
  }

  // received a Map
  if (isMap(collection)) {
    let i = 0;
    for (const [ key, value ] of collection.entries()) {
      if (predicate(value, key, i++)) return true;
    }
    return false;
  }

  // received an object hash
  if (isObject(collection)) {
    let i = 0;
    for (const [ key, value ] of Object.entries(collection)) {
      if (predicate(value, key, i++)) return true;
    }
    return false;
  }

  return !!collection;
}

function iteratee (match) {
  if (isUndefined(match) || match === null) return Boolean;

  if (isFunction(match)) return match;

  if (isString(match)) {
    return (o) => {
      if (isArray(o)) return o.includes(match);
      if (isObject(o)) return o[match];
      if (isMap(o)) return o.get(match);
      if (isSet(o)) return o.has(match);
      if (isPrimitive(o)) return o[match];
      return o === match;
    };
  }

  if (isNumber(match)) {
    return (o) => {
      if (isObject(o) || isArray(o)) return o[match];
      if (isMap(o)) return o.get(match);
      if (isSet(o)) return o.has(match);
      if (isNumber(o)) return o === match;
      if (isString(o)) return Number(o) === match;
      return o === match;
    };
  }

  if (isArray(match)) {
    const [ key, value ] = match;
    return (o) => o[key] === value;
  }

  if (isObject(match)) {
    // create an array of key/value iteratees
    const tests = Object.entries(match).map(iteratee);
    // evaluate the object against the array
    return (o) => {
      for (const t of tests) {
        if (!t(o)) return false;
      }
      return true;
    };
  }
}

function sorter (match) {

  if (isFunction(match)) return match;

  function qs (a, b) {
    if (a > b) return 1;
    else if (b > a) return -1;
    return 0;
  }

  if (isString(match)) {
    return (a, b) => {
      if (!isObject(a) && !isObject(b)) return qs(a, b);
      if (!isObject(a)) return -1;
      if (!isObject(b)) return 1;
      return qs(a[match], b[match]);
    };
  }

  if (isArray(match)) {
    return (a, b) => {
      if (!isObject(a) && !isObject(b)) return qs(a, b);
      if (!isObject(a)) return -1;
      if (!isObject(b)) return 1;
      for (const k of match) {
        const v = qs(a[k], b[k]);
        if (v) return v;
      }
      return 0;
    };
  }

  if (isObject(match)) {
    return (a, b) => {
      if (!isObject(a) && !isObject(b)) return qs(a, b);
      if (!isObject(a)) return -1;
      if (!isObject(b)) return 1;
      for (const [ k, d ] of Object.entries(match)) {
        const v = qs(a[k], b[k]) * (d < 0 ? -1 : 1);
        if (v) return v;
      }
      return 0;
    };
  }

  return (a, b) => {
    if (!isObject(a) && !isObject(b)) return qs(a, b);
    if (!isObject(a)) return -1;
    if (!isObject(b)) return 1;
    return 0;
  };
}

function toPairs (object) {
  return Object.entries(object);
}

function fromPairs (entries) {
  return mapReduce(entries, ([ v, k ]) => [ v, k ]);
}

function slice (collection, begin, end) {
  if (isString(collection) || isArray(collection)) return collection.slice(begin, end);

  if (isSet(collection)) {
    return new Set(Array.from(collection.values()).slice(begin, end));
  }

  if (isMap(collection)) {
    return new Map(Array.from(collection.entries()).slice(begin, end));
  }

  if (isObject(collection)) {
    return fromPairs(toPairs(collection).slice(begin, end));
  }

  return collection;
}

function sort (collection, predicate) {

  predicate = sorter(predicate);

  if (isArray(collection)) return [ ...collection ].sort(predicate);

  if (isSet(collection)) {
    return new Set(Array.from(collection.values()).sort(predicate));
  }

  // sort by key for maps and objects
  const hashpredicate = (a, b) => predicate(a[0], b[0]);

  if (isMap(collection)) {
    return new Map(Array.from(collection.entries()).sort(hashpredicate));
  }

  if (isObject(collection)) {
    return fromPairs(toPairs(collection).sort(hashpredicate));
  }

  return collection;
}

function map (collection, predicate) {
  predicate = iteratee(predicate);

  if (isArray(collection)) {
    return collection.map((value, i) => predicate(value, i, i));
  }

  if (isSet(collection)) {
    return Array.from(collection, (value, i) => predicate(value, i, i));
  }

  return mapReduce(collection, (value, key, index) => [ key, predicate(value, key, index) ]);
}

function uniq (collection, predicate = null) {
  if (predicate === null) {
    predicate = (v) => v;
  } else {
    predicate = iteratee(predicate);
  }

  const exists = new Set();

  if (isArray(collection)) {
    const result = [];
    collection.forEach((v) => {
      const match = predicate(v);
      if (exists.has(match)) return;
      exists.add(match);
      result.push(v);
    });

    return result;
  }

  if (isSet(collection)) return new Set(collection); // really?

  if (isMap(collection)) {
    return new Map(Array.from(collection.entries(), ([ k, v ]) => {
      const match = predicate(v);
      if (exists.has(match)) return false;
      exists.add(match);
      return [ k, v ];
    }).filter(Boolean));
  }

  if (isObject(collection)) {
    return mapReduce(collection, ([ v, k ]) => {
      const match = predicate(v);
      if (exists.has(match)) return null;
      exists.add(match);
      return [ k, v ];
    });
  }

  return collection;
}

function keyBy (collection, predicate) {
  predicate = iteratee(predicate);
  return mapReduce(collection, (value, key, index) =>
    [ predicate(value, key, index), value ]
  );
}

function groupBy (collection, predicate) {
  predicate = iteratee(predicate);
  return reduce(collection, (result, value, key, index) => {
    const k = predicate(value, key, index);
    (result[k] || (result[k] = [])).push(value);
    return result;
  }, {});
}

function filter (collection, predicate) {
  predicate = iteratee(predicate);

  if (isArray(collection)) {
    return collection.filter((value, i) => predicate(value, i, i));
  }

  if (isSet(collection)) {
    return Array.from(collection).filter((value, i) => predicate(value, i, i));
  }

  throw new Error('filter can not be applied to objects or maps, perhaps you meant to use omit?');
}

function omit (collection, predicate) {
  if (isFunction(predicate)) {
    return mapReduce(collection, (value, key, index) =>
      (predicate(value, key, index)
        ? [ undefined, undefined ]
        : [ key, value ])
    );
  }

  if (isString(predicate)) {
    predicate = [ predicate ];
  }

  if (!isArray(predicate)) throw new Error('omit requires a string or array of strings');
  return mapReduce(collection, (value, key) =>
    (predicate.includes(key)
      ? [ undefined, undefined ]
      : [ key, value ])
  );
}

function pick (collection, predicate) {
  if (!collection) return {};

  if (isFunction(predicate)) {
    return mapReduce(collection, (value, key, index) =>
      (predicate(value, key, index)
        ? [ key, value ]
        : [ undefined, undefined ])
    );
  }

  if (isString(predicate)) {
    predicate = [ predicate ];
  }

  if (!isArray(predicate)) throw new Error('pick requires a string or array of strings');
  return predicate.reduce((obj, key) => {
    const value = get(collection, key);
    if (isUndefined(value)) return obj;
    return set(obj, key, value);
  }, {});
}


function deepPick (collection, schema) {
  if (isPrimitive(schema) && isPrimitive(collection)) return collection;

  if (isArray(schema) && schema.length > 0) {
    // collection does not match this schema tier, abort
    if (!isArray(collection)) return;

    schema = schema[0];
    return collection.map((branch) => deepPick(branch, schema));
  }

  // if the schema at this tier is not an object,
  // return the value at this tier only if schema is truthy
  if (!isObject(schema)) return schema ? collection : undefined;
  if (isPrimitive(collection)) return;

  // if the collection isn't something we can pull data from, skip it
  if (!isObject(collection) && !isFunction(collection)) return;

  const result = {};
  for (const [ key, subschema ] of Object.entries(schema)) {

    const target = collection[key];
    if (isUndefined(target)) continue;

    const child = deepPick(target, subschema);
    if (isUndefined(child)) continue;

    result[key] = child;
  }

  return result;
}



function pathinate (object, delimiter = '.') {
  const paths = [];

  function descend (branch, ancest) {
    if (!isObject(branch)) {
      paths.push(ancest.join(delimiter));
      return;
    }
    for (const [ k, v ] of Object.entries(branch)) {
      descend(v, ancest.concat([ k ]));
    }
    return;
  }

  descend(object, []);

  return uniq(paths);
}



/**
 * Iterates over a collection and generates an object based on tuple returned from the iteratee.
 *
 * @param  {Object|Array|Map|Set} collection
 * @param  {Function} cb Callback invoked for each item, receives `value, key, index`, returns `[key, value]`;
 * @returns {Object}
 */
function mapReduce (collection, cb) {
  if (!collection) return {};

  const result = {};
  function iterate (v, k, i) {
    // return true to continue looping
    const res = cb(v, k, i) || [];
    if (res === false) return false;
    if (!res || !isArray(res)) return true;
    const [ key, value ] = res;
    if (key === undefined || key === null || value === undefined) return true;
    result[key] = value;
    return true;
  }

  if (isArray(collection)) {
    let i = 0;
    for (const value of collection) {
      if (!iterate(value, i, i++)) break;
    }
    return result;
  }

  if (isSet(collection)) {
    let i = 0;
    for (const item of collection) {
      if (!iterate(item, i, i++)) break;
    }
    return result;
  }

  // received a Map
  if (isMap(collection)) {
    let i = 0;
    for (const [ key, value ] of collection.entries()) {
      if (!iterate(value, key, i++)) break;
    }
    return result;
  }

  // received an object hash
  if (isObject(collection)) {
    let i = 0;
    for (const [ key, value ] of Object.entries(collection)) {
      if (!iterate(value, key, i++)) break;
    }
    return result;
  }

  return result;
}

function reduce (collection, predicate, init) {
  if (!isFunction(predicate)) throw new TypeError('Predicate must be a function');

  if (isArray(collection)) return collection.reduce((r, v, i) => predicate(r, v, i, i), init);

  if (isSet(collection)) {
    return Array.from(collection).reduce((r, v, i) => predicate(r, v, i, i), init);
  }

  if (isMap(collection)) {
    return Array.from(collection.entries()).reduce((prev, [ key, value ], i) => predicate(prev, value, key, i), init);
  }

  if (isObject(collection)) {
    return Object.entries(collection).reduce((prev, [ key, value ], i) => predicate(prev, value, key, i), init);
  }
}

function flatten (collection, depth = Infinity) {
  if (depth <= 0) return slice(collection);
  return reduce(collection,
    (acc, val) => acc.concat(...(
      isMappable(val)
        ? flatten(val, depth - 1)
        : [ val ]
    )),
    []
  );
}

function slugify (input, delimiter = '-', separators = false) {
  var i = separators && separators.length;
  var slug = input;
  var regexEscape = new RegExp(/[[/\\^$*+?.()|{}\]]/g);
  var regexDelimiter = delimiter.replace(regexEscape, '\\$&');
  var prohibited = new RegExp('([^a-z0-9' + regexDelimiter + '])', 'g');
  var consecutive = new RegExp('(' + regexDelimiter + '+)', 'g');
  var trim = new RegExp('^' + regexDelimiter + '*(.*?)' + regexDelimiter + '*$');
  var sanitizer = {
    // common latin
    'á': 'a',
    'à': 'a',
    'â': 'a',
    'ä': 'a',
    'ã': 'a',
    'æ': 'ae',
    'ç': 'c',
    'é': 'e',
    'è': 'e',
    'ê': 'e',
    'ë': 'e',
    'ẽ': 'e',
    'í': 'i',
    'ì': 'i',
    'î': 'i',
    'ï': 'i',
    'ĩ': 'i',
    'ó': 'o',
    'ò': 'o',
    'ô': 'o',
    'ö': 'o',
    'õ': 'o',
    'œ': 'oe',
    'ß': 'ss',
    'ú': 'u',
    'ù': 'u',
    'û': 'u',
    'ü': 'u',
    'ũ': 'u',

    // other diacritics
    'ă': 'a',
    'ắ': 'a',
    'ằ': 'a',
    'ẵ': 'a',
    'ẳ': 'a',
    'ấ': 'a',
    'ầ': 'a',
    'ẫ': 'a',
    'ẩ': 'a',
    'ǎ': 'a',
    'å': 'a',
    'ǻ': 'a',
    'ǟ': 'a',
    'ȧ': 'a',
    'ǡ': 'a',
    'ą': 'a',
    'ā': 'a',
    'ả': 'a',
    'ȁ': 'a',
    'ȃ': 'a',
    'ạ': 'a',
    'ặ': 'a',
    'ậ': 'a',
    'ḁ': 'a',
    'ⱥ': 'a',
    'ᶏ': 'a',
    'ɐ': 'a',
    'ɑ': 'a',

    'ḃ': 'b',
    'ḅ': 'b',
    'ḇ': 'b',
    'ƀ': 'b',
    'ɓ': 'b',
    'ƃ': 'b',
    'ᵬ': 'b',
    'ᶀ': 'b',
    'þ': 'b',

    'ć': 'c',
    'ĉ': 'c',
    'č': 'c',
    'ċ': 'c',
    'ḉ': 'c',
    'ȼ': 'c',
    'ƈ': 'c',
    'ɕ': 'c',

    'ď': 'd',
    'ḋ': 'd',
    'ḑ': 'd',
    'ḍ': 'd',
    'ḓ': 'd',
    'ḏ': 'd',
    'đ': 'd',
    'ɖ': 'd',
    'ɗ': 'd',
    'ƌ': 'd',
    'ᵭ': 'd',
    'ᶁ': 'd',
    'ᶑ': 'd',
    'ȡ': 'd',
    '∂': 'd',

    'ĕ': 'e',
    'ế': 'e',
    'ề': 'e',
    'ễ': 'e',
    'ể': 'e',
    'ě': 'e',
    'ė': 'e',
    'ȩ': 'e',
    'ḝ': 'e',
    'ę': 'e',
    'ē': 'e',
    'ḗ': 'e',
    'ḕ': 'e',
    'ẻ': 'e',
    'ȅ': 'e',
    'ȇ': 'e',
    'ẹ': 'e',
    'ệ': 'e',
    'ḙ': 'e',
    'ḛ': 'e',
    'ɇ': 'e',
    'ᶒ': 'e',

    'ḟ': 'f',
    'ƒ': 'f',
    'ᵮ': 'f',
    'ᶂ': 'f',

    'ǵ': 'g',
    'ğ': 'g',
    'ĝ': 'g',
    'ǧ': 'g',
    'ġ': 'g',
    'ģ': 'g',
    'ḡ': 'g',
    'ǥ': 'g',
    'ɠ': 'g',
    'ᶃ': 'g',

    'ĥ': 'h',
    'ȟ': 'h',
    'ḧ': 'h',
    'ḣ': 'h',
    'ḩ': 'h',
    'ḥ': 'h',
    'ḫ': 'h',
    'ẖ': 'h',
    'ħ': 'h',
    'ⱨ': 'h',

    'ĭ': 'i',
    'ǐ': 'i',
    'ḯ': 'i',
    'į': 'i',
    'ī': 'i',
    'ỉ': 'i',
    'ȉ': 'i',
    'ȋ': 'i',
    'ị': 'i',
    'ḭ': 'i',
    'ɨ': 'i',
    'ᵻ': 'i',
    'ᶖ': 'i',
    'i': 'i',
    'ı': 'i',

    'ĵ': 'j',
    'ɉ': 'j',
    'ǰ': 'j',
    'ȷ': 'j',
    'ʝ': 'j',
    'ɟ': 'j',
    'ʄ': 'j',

    'ḱ': 'k',
    'ǩ': 'k',
    'ķ': 'k',
    'ḳ': 'k',
    'ḵ': 'k',
    'ƙ': 'k',
    'ⱪ': 'k',
    'ᶄ': 'k',

    'ĺ': 'l',
    'ľ': 'l',
    'ļ': 'l',
    'ḷ': 'l',
    'ḹ': 'l',
    'ḽ': 'l',
    'ḻ': 'l',
    'ł': 'l',
    'ŀ': 'l',
    'ƚ': 'l',
    'ⱡ': 'l',
    'ɫ': 'l',
    'ɬ': 'l',
    'ᶅ': 'l',
    'ɭ': 'l',
    'ȴ': 'l',

    'ḿ': 'm',
    'ṁ': 'm',
    'ṃ': 'm',
    'ᵯ': 'm',
    'ᶆ': 'm',
    'ɱ': 'm',

    'ń': 'n',
    'ǹ': 'n',
    'ň': 'n',
    'ñ': 'n',
    'ṅ': 'n',
    'ņ': 'n',
    'ṇ': 'n',
    'ṋ': 'n',
    'ṉ': 'n',
    'n̈': 'n',
    'ɲ': 'n',
    'ƞ': 'n',
    'ŋ': 'n',
    'ᵰ': 'n',
    'ᶇ': 'n',
    'ɳ': 'n',
    'ȵ': 'n',

    'ŏ': 'o',
    'ố': 'o',
    'ồ': 'o',
    'ỗ': 'o',
    'ổ': 'o',
    'ǒ': 'o',
    'ȫ': 'o',
    'ő': 'o',
    'ṍ': 'o',
    'ṏ': 'o',
    'ȭ': 'o',
    'ȯ': 'o',
    '͘o͘': 'o',
    'ȱ': 'o',
    'ø': 'o',
    'ǿ': 'o',
    'ǫ': 'o',
    'ǭ': 'o',
    'ō': 'o',
    'ṓ': 'o',
    'ṑ': 'o',
    'ỏ': 'o',
    'ȍ': 'o',
    'ȏ': 'o',
    'ơ': 'o',
    'ớ': 'o',
    'ờ': 'o',
    'ỡ': 'o',
    'ở': 'o',
    'ợ': 'o',
    'ọ': 'o',
    'ộ': 'o',
    'ɵ': 'o',
    'ɔ': 'o',

    'ṕ': 'p',
    'ṗ': 'p',
    'ᵽ': 'p',
    'ƥ': 'p',
    'p̃': 'p',
    'ᵱ': 'p',
    'ᶈ': 'p',

    'ɋ': 'q',
    'ƣ': 'q',
    'ʠ': 'q',

    'ŕ': 'r',
    'ř': 'r',
    'ṙ': 'r',
    'ŗ': 'r',
    'ȑ': 'r',
    'ȓ': 'r',
    'ṛ': 'r',
    'ṝ': 'r',
    'ṟ': 'r',
    'ɍ': 'r',
    'ɽ': 'r',
    'ᵲ': 'r',
    'ᶉ': 'r',
    'ɼ': 'r',
    'ɾ': 'r',
    'ᵳ': 'r',

    'ś': 's',
    'ṥ': 's',
    'ŝ': 's',
    'š': 's',
    'ṧ': 's',
    'ṡẛ': 's',
    'ş': 's',
    'ṣ': 's',
    'ṩ': 's',
    'ș': 's',
    's̩': 's',
    'ᵴ': 's',
    'ᶊ': 's',
    'ʂ': 's',
    'ȿ': 's',

    'ť': 't',
    'ṫ': 't',
    'ţ': 't',
    'ṭ': 't',
    'ț': 't',
    'ṱ': 't',
    'ṯ': 't',
    'ŧ': 't',
    'ⱦ': 't',
    'ƭ': 't',
    'ʈ': 't',
    '̈ẗ': 't',
    'ᵵ': 't',
    'ƫ': 't',
    'ȶ': 't',

    'ŭ': 'u',
    'ǔ': 'u',
    'ů': 'u',
    'ǘ': 'u',
    'ǜ': 'u',
    'ǚ': 'u',
    'ǖ': 'u',
    'ű': 'u',
    'ṹ': 'u',
    'ų': 'u',
    'ū': 'u',
    'ṻ': 'u',
    'ủ': 'u',
    'ȕ': 'u',
    'ȗ': 'u',
    'ư': 'u',
    'ứ': 'u',
    'ừ': 'u',
    'ữ': 'u',
    'ử': 'u',
    'ự': 'u',
    'ụ': 'u',
    'ṳ': 'u',
    'ṷ': 'u',
    'ṵ': 'u',
    'ʉ': 'u',
    'ᵾ': 'u',
    'ᶙ': 'u',

    'ṽ': 'v',
    'ṿ': 'v',
    'ʋ': 'v',
    'ᶌ': 'v',
    'ⱴ': 'v',

    'ẃ': 'w',
    'ẁ': 'w',
    'ŵ': 'w',
    'ẅ': 'w',
    'ẇ': 'w',
    'ẉ': 'w',
    'ẘ': 'w',

    'ẍ': 'x',
    'ẋ': 'x',
    'ᶍ': 'x',

    'ý': 'y',
    'ỳ': 'y',
    'ŷ': 'y',
    'ẙ': 'y',
    'ÿ': 'y',
    'ỹ': 'y',
    'ẏ': 'y',
    'ȳ': 'y',
    'ỷ': 'y',
    'ỵ': 'y',
    'ɏ': 'y',
    'ƴ': 'y',
    'ʏ': 'y',

    'ź': 'z',
    'ẑ': 'z',
    'ž': 'z',
    'ż': 'z',
    'ẓ': 'z',
    'ẕ': 'z',
    'ƶ': 'z',
    'ȥ': 'z',
    'ⱬ': 'z',
    'ᵶ': 'z',
    'ᶎ': 'z',
    'ʐ': 'z',
    'ʑ': 'z',
    'ɀ': 'z',

    // greek
    'α': 'a',
    'β': 'b',
    'γ': 'g',
    'ɣ': 'g',
    'δ': 'd',
    'ð': 'd',
    'ε': 'e',
    'ζ': 'z',
    'η': 'i',
    'θ': 'th',
    'ι': 'i',
    'κ': 'k',
    'λ': 'l',
    'μ': 'm',
    'µ': 'm',
    'ν': 'n',
    'ξ': 'x',
    'ο': 'o',
    'π': 'p',
    'ρ': 'r',
    'σ': 's',
    'ς': 's',
    'τ': 't',
    'υ': 'u', // official rule: if preceeded by 'α' OR 'ε' => 'v', by 'ο' => 'u', else => 'i'
    'φ': 'f',
    'χ': 'ch',
    'ψ': 'ps',
    'ω': 'o',

    // greek diacritics
    'ᾳ': 'a',
    'ά': 'a',
    'ὰ': 'a',
    'ᾴ': 'a',
    'ᾲ': 'a',
    'ᾶ': 'a',
    'ᾷ': 'a',
    'ἀ': 'a',
    'ᾀ': 'a',
    'ἄ': 'a',
    'ᾄ': 'a',
    'ἂ': 'a',
    'ᾂ': 'a',
    'ἆ': 'a',
    'ᾆ': 'a',
    'ἁ': 'a',
    'ᾁ': 'a',
    'ἅ': 'a',
    'ᾅ': 'a',
    'ἃ': 'a',
    'ᾃ': 'a',
    'ἇ': 'a',
    'ᾇ': 'a',
    'ᾱ': 'a',
    'ᾰ': 'a',

    'έ': 'e',
    'ὲ': 'e',
    'ἐ': 'e',
    'ἔ': 'e',
    'ἒ': 'e',
    'ἑ': 'e',
    'ἕ': 'e',
    'ἓ': 'e',

    'ῃ': 'i',
    'ή': 'i',
    'ὴ': 'i',
    'ῄ': 'i',
    'ῂ': 'i',
    'ῆ': 'i',
    'ῇ': 'i',
    'ἠ': 'i',
    'ᾐ': 'i',
    'ἤ': 'i',
    'ᾔ': 'i',
    'ἢ': 'i',
    'ᾒ': 'i',
    'ἦ': 'i',
    'ᾖ': 'i',
    'ἡ': 'i',
    'ᾑ': 'i',
    'ἥ': 'i',
    'ᾕ': 'i',
    'ἣ': 'i',
    'ᾓ': 'i',
    'ἧ': 'i',
    'ᾗ': 'i',

    'ί': 'i',
    'ὶ': 'i',
    'ῖ': 'i',
    'ἰ': 'i',
    'ἴ': 'i',
    'ἲ': 'i',
    'ἶ': 'i',
    'ἱ': 'i',
    'ἵ': 'i',
    'ἳ': 'i',
    'ἷ': 'i',
    'ϊ': 'i',
    'ΐ': 'i',
    'ῒ': 'i',
    'ῗ': 'i',
    'ῑ': 'i',
    'ῐ': 'i',

    'ό': 'o',
    'ὸ': 'o',
    'ὀ': 'o',
    'ὄ': 'o',
    'ὂ': 'o',
    'ὁ': 'o',
    'ὅ': 'o',
    'ὃ': 'o',

    'ύ': 'u',
    'ὺ': 'u',
    'ῦ': 'u',
    'ὐ': 'u',
    'ὔ': 'u',
    'ὒ': 'u',
    'ὖ': 'u',
    'ὑ': 'u',
    'ὕ': 'u',
    'ὓ': 'u',
    'ὗ': 'u',
    'ϋ': 'u',
    'ΰ': 'u',
    'ῢ': 'u',
    'ῧ': 'u',
    'ῡ': 'u',
    'ῠ': 'u',

    'ῳ': 'o',
    'ώ': 'o',
    'ῴ': 'o',
    'ὼ': 'o',
    'ῲ': 'o',
    'ῶ': 'o',
    'ῷ': 'o',
    'ὠ': 'o',
    'ᾠ': 'o',
    'ὤ': 'o',
    'ᾤ': 'o',
    'ὢ': 'o',
    'ᾢ': 'o',
    'ὦ': 'o',
    'ᾦ': 'o',
    'ὡ': 'o',
    'ᾡ': 'o',
    'ὥ': 'o',
    'ᾥ': 'o',
    'ὣ': 'o',
    'ᾣ': 'o',
    'ὧ': 'o',
    'ᾧ': 'o',

    'ῤ': 'r',
    'ῥ': 'r',

    // cyrillic (russian)
    'а': 'a',
    'б': 'b',
    'в': 'v',
    'г': 'g',
    'д': 'd',
    'е': 'e',
    'ё': 'e',
    'ж': 'zh',
    'з': 'z',
    'и': 'i',
    'й': 'j',
    'к': 'k',
    'л': 'l',
    'м': 'm',
    'н': 'n',
    'о': 'o',
    'п': 'p',
    'р': 'r',
    'с': 's',
    'т': 't',
    'у': 'u',
    'ф': 'f',
    'х': 'h',
    'ц': 'ts',
    'ч': 'ch',
    'ш': 'sh',
    'щ': 'sh',
    'ъ': '',
    'ы': 'i',
    'ь': '',
    'э': 'e',
    'ю': 'yu',
    'я': 'ya',
    // ---
    'і': 'j',
    'ѳ': 'f',
    'ѣ': 'e',
    'ѵ': 'i',
    'ѕ': 'z',
    'ѯ': 'ks',
    'ѱ': 'ps',
    'ѡ': 'o',
    'ѫ': 'yu',
    'ѧ': 'ya',
    'ѭ': 'yu',
    'ѩ': 'ya',

    // currency
    '₳': 'ARA',
    '฿': 'THB',
    '₵': 'GHS',
    '¢': 'c',
    '₡': 'CRC',
    '₢': 'Cr',
    '₠': 'XEU',
    '$': 'USD',
    '₫': 'VND',
    '৳': 'BDT',
    '₯': 'GRD',
    '€': 'EUR',
    '₣': 'FRF',
    '₲': 'PYG',
    '₴': 'HRN',
    '₭': 'LAK',
    '₦': 'NGN',
    '₧': 'ESP',
    '₱': 'PhP',
    '£': 'GBP',
    '₤': 'GBP',
    '₨': 'Rs',
    '₪': 'NS',
    '₮': 'MNT',
    '₩': 'WON',
    '¥': 'YEN',
    '៛': 'KHR',

    // separators
    '–': delimiter,
    '—': delimiter,
    '―': delimiter,
    '~': delimiter,
    '/': delimiter,
    '\\': delimiter,
    '|': delimiter,
    '+': delimiter,
    '‘': delimiter,
    '’': delimiter,
    '\'': delimiter,
    ' ': delimiter,

    // permitted by default but can be overridden
    '-': '-',
    '_': '_',
  };

  // add any user-defined separator elements
  if (separators) {
    for (i; i >= 0; --i) {
      sanitizer[separators[i]] = delimiter;
    }
  }

  // do all the replacements
  slug = slug.toLowerCase(); // if we don't do this, add the uppercase versions to the sanitizer plus inlcude A-Z in the prohibited filter
  slug = slug.replace(prohibited, (match) => sanitizer[match] || '');
  slug = slug.replace(consecutive, delimiter);
  slug = slug.replace(trim, '$1');

  return slug;
}

exports.all = all;
exports.allBy = allBy;
exports.allOf = allOf;
exports.any = any;
exports.anyBy = anyBy;
exports.anyOf = anyOf;
exports.arrayify = arrayify;
exports.contains = contains;
exports.containsBooleans = containsBooleans;
exports.containsFalsey = containsFalsey;
exports.containsFunctions = containsFunctions;
exports.containsMappables = containsMappables;
exports.containsNumbers = containsNumbers;
exports.containsObjects = containsObjects;
exports.containsPrimatives = containsPrimatives;
exports.containsRegEx = containsRegEx;
exports.containsStrings = containsStrings;
exports.containsTruthy = containsTruthy;
exports.deepPick = deepPick;
exports.equals = equals;
exports.filter = filter;
exports.first = first;
exports.flatten = flatten;
exports.fromPairs = fromPairs;
exports.get = get;
exports.groupBy = groupBy;
exports.has = has;
exports.hasOwn = hasOwn;
exports.is = is;
exports.isAll = isAll;
exports.isArray = isArray;
exports.isArrayOf = isArrayOf;
exports.isArrayOfBooleans = isArrayOfBooleans;
exports.isArrayOfFalsey = isArrayOfFalsey;
exports.isArrayOfFunctions = isArrayOfFunctions;
exports.isArrayOfMappables = isArrayOfMappables;
exports.isArrayOfNumbers = isArrayOfNumbers;
exports.isArrayOfObjects = isArrayOfObjects;
exports.isArrayOfPrimatives = isArrayOfPrimatives;
exports.isArrayOfRegEx = isArrayOfRegEx;
exports.isArrayOfStrings = isArrayOfStrings;
exports.isArrayOfTruthy = isArrayOfTruthy;
exports.isBoolean = isBoolean;
exports.isDate = isDate;
exports.isFalsey = isFalsey;
exports.isFunction = isFunction;
exports.isMap = isMap;
exports.isMappable = isMappable;
exports.isNull = isNull;
exports.isNumber = isNumber;
exports.isObject = isObject;
exports.isPrimitive = isPrimitive;
exports.isRegExp = isRegExp;
exports.isSet = isSet;
exports.isString = isString;
exports.isTruthy = isTruthy;
exports.isUndefined = isUndefined;
exports.iteratee = iteratee;
exports.keyBy = keyBy;
exports.keys = keys;
exports.last = last;
exports.lc = lc;
exports.map = map;
exports.mapReduce = mapReduce;
exports.merge = merge;
exports.omit = omit;
exports.pathinate = pathinate;
exports.pick = pick;
exports.re = re;
exports.reduce = reduce;
exports.set = set;
exports.sizeOf = sizeOf;
exports.slice = slice;
exports.slugify = slugify;
exports.sort = sort;
exports.sorter = sorter;
exports.toPairs = toPairs;
exports.truthy = truthy;
exports.uc = uc;
exports.ucfirst = ucfirst;
exports.ucsentence = ucsentence;
exports.ucwords = ucwords;
exports.uniq = uniq;
exports.values = values;