'use strict'; const Assert = require('./assert'); const DeepEqual = require('./deepEqual'); const EscapeRegex = require('./escapeRegex'); const Utils = require('./utils'); const internals = {}; module.exports = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols } /* string -> string(s) array -> item(s) object -> key(s) object -> object (key:value) */ let valuePairs = null; if (typeof ref === 'object' && typeof values === 'object' && !Array.isArray(ref) && !Array.isArray(values)) { valuePairs = values; const symbols = Object.getOwnPropertySymbols(values).filter(Object.prototype.propertyIsEnumerable.bind(values)); values = [...Object.keys(values), ...symbols]; } else { values = [].concat(values); } Assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object'); Assert(values.length, 'Values array cannot be empty'); let compare; let compareFlags; if (options.deep) { compare = DeepEqual; const hasOnly = options.only !== undefined; const hasPart = options.part !== undefined; compareFlags = { prototype: hasOnly ? options.only : hasPart ? !options.part : false, part: hasOnly ? !options.only : hasPart ? options.part : false }; } else { compare = (a, b) => a === b; } let misses = false; const matches = new Array(values.length); for (let i = 0; i < matches.length; ++i) { matches[i] = 0; } if (typeof ref === 'string') { if (ref === '') { if (values.length === 1 && values[0] === '' || // '' contains '' !options.once && !values.some((v) => v !== '')) { // '' contains multiple '' if !once return true; } return false; } let pattern = '('; for (let i = 0; i < values.length; ++i) { const value = values[i]; Assert(typeof value === 'string', 'Cannot compare string reference to non-string value'); pattern += (i ? '|' : '') + EscapeRegex(value); } const regex = new RegExp(pattern + ')', 'g'); const leftovers = ref.replace(regex, ($0, $1) => { const index = values.indexOf($1); ++matches[index]; return ''; // Remove from string }); misses = !!leftovers; } else if (Array.isArray(ref)) { if (!ref.length) { return false; } const onlyOnce = !!(options.only && options.once); if (onlyOnce && ref.length !== values.length) { return false; } for (let i = 0; i < ref.length; ++i) { let matched = false; for (let j = 0; j < values.length && matched === false; ++j) { if (!onlyOnce || matches[j] === 0) { matched = compare(values[j], ref[i], compareFlags) && j; } } if (matched !== false) { ++matches[matched]; } else { misses = true; } } } else { const keys = Utils.keys(ref, options); if (!keys.length) { return false; } for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const pos = values.indexOf(key); if (pos !== -1) { if (valuePairs && !compare(valuePairs[key], ref[key], compareFlags)) { return false; } ++matches[pos]; } else { misses = true; } } } if (options.only) { if (misses || !options.once) { return !misses; } } let result = false; for (let i = 0; i < matches.length; ++i) { result = result || !!matches[i]; if ((options.once && matches[i] > 1) || (!options.part && !matches[i])) { return false; } } return result; };