/**
 * 
 * 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' && !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} iteratee Callback invoked for each item, receives `value, key, index`, returns `[key, value]`;
 * @return {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;