| '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) | 
|     */ | 
|   | 
|     if (typeof values !== 'object') { | 
|         values = [values]; | 
|     } | 
|   | 
|     Assert(!Array.isArray(values) || values.length, 'Values array cannot be empty'); | 
|   | 
|     // String | 
|   | 
|     if (typeof ref === 'string') { | 
|         return internals.string(ref, values, options); | 
|     } | 
|   | 
|     // Array | 
|   | 
|     if (Array.isArray(ref)) { | 
|         return internals.array(ref, values, options); | 
|     } | 
|   | 
|     // Object | 
|   | 
|     Assert(typeof ref === 'object', 'Reference must be string or an object'); | 
|     return internals.object(ref, values, options); | 
| }; | 
|   | 
|   | 
| internals.array = function (ref, values, options) { | 
|   | 
|     if (!Array.isArray(values)) { | 
|         values = [values]; | 
|     } | 
|   | 
|     if (!ref.length) { | 
|         return false; | 
|     } | 
|   | 
|     if (options.only && | 
|         options.once && | 
|         ref.length !== values.length) { | 
|   | 
|         return false; | 
|     } | 
|   | 
|     let compare; | 
|   | 
|     // Map values | 
|   | 
|     const map = new Map(); | 
|     for (const value of values) { | 
|         if (!options.deep || | 
|             !value || | 
|             typeof value !== 'object') { | 
|   | 
|             const existing = map.get(value); | 
|             if (existing) { | 
|                 ++existing.allowed; | 
|             } | 
|             else { | 
|                 map.set(value, { allowed: 1, hits: 0 }); | 
|             } | 
|         } | 
|         else { | 
|             compare = compare || internals.compare(options); | 
|   | 
|             let found = false; | 
|             for (const [key, existing] of map.entries()) { | 
|                 if (compare(key, value)) { | 
|                     ++existing.allowed; | 
|                     found = true; | 
|                     break; | 
|                 } | 
|             } | 
|   | 
|             if (!found) { | 
|                 map.set(value, { allowed: 1, hits: 0 }); | 
|             } | 
|         } | 
|     } | 
|   | 
|     // Lookup values | 
|   | 
|     let hits = 0; | 
|     for (const item of ref) { | 
|         let match; | 
|         if (!options.deep || | 
|             !item || | 
|             typeof item !== 'object') { | 
|   | 
|             match = map.get(item); | 
|         } | 
|         else { | 
|             for (const [key, existing] of map.entries()) { | 
|                 if (compare(key, item)) { | 
|                     match = existing; | 
|                     break; | 
|                 } | 
|             } | 
|         } | 
|   | 
|         if (match) { | 
|             ++match.hits; | 
|             ++hits; | 
|   | 
|             if (options.once && | 
|                 match.hits > match.allowed) { | 
|   | 
|                 return false; | 
|             } | 
|         } | 
|     } | 
|   | 
|     // Validate results | 
|   | 
|     if (options.only && | 
|         hits !== ref.length) { | 
|   | 
|         return false; | 
|     } | 
|   | 
|     for (const match of map.values()) { | 
|         if (match.hits === match.allowed) { | 
|             continue; | 
|         } | 
|   | 
|         if (match.hits < match.allowed && | 
|             !options.part) { | 
|   | 
|             return false; | 
|         } | 
|     } | 
|   | 
|     return !!hits; | 
| }; | 
|   | 
|   | 
| internals.object = function (ref, values, options) { | 
|   | 
|     Assert(options.once === undefined, 'Cannot use option once with object'); | 
|   | 
|     const keys = Utils.keys(ref, options); | 
|     if (!keys.length) { | 
|         return false; | 
|     } | 
|   | 
|     // Keys list | 
|   | 
|     if (Array.isArray(values)) { | 
|         return internals.array(keys, values, options); | 
|     } | 
|   | 
|     // Key value pairs | 
|   | 
|     const symbols = Object.getOwnPropertySymbols(values).filter((sym) => values.propertyIsEnumerable(sym)); | 
|     const targets = [...Object.keys(values), ...symbols]; | 
|   | 
|     const compare = internals.compare(options); | 
|     const set = new Set(targets); | 
|   | 
|     for (const key of keys) { | 
|         if (!set.has(key)) { | 
|             if (options.only) { | 
|                 return false; | 
|             } | 
|   | 
|             continue; | 
|         } | 
|   | 
|         if (!compare(values[key], ref[key])) { | 
|             return false; | 
|         } | 
|   | 
|         set.delete(key); | 
|     } | 
|   | 
|     if (set.size) { | 
|         return options.part ? set.size < targets.length : false; | 
|     } | 
|   | 
|     return true; | 
| }; | 
|   | 
|   | 
| internals.string = function (ref, values, options) { | 
|   | 
|     // Empty string | 
|   | 
|     if (ref === '') { | 
|         return values.length === 1 && values[0] === '' ||               // '' contains '' | 
|             !options.once && !values.some((v) => v !== '');             // '' contains multiple '' if !once | 
|     } | 
|   | 
|     // Map values | 
|   | 
|     const map = new Map(); | 
|     const patterns = []; | 
|   | 
|     for (const value of values) { | 
|         Assert(typeof value === 'string', 'Cannot compare string reference to non-string value'); | 
|   | 
|         if (value) { | 
|             const existing = map.get(value); | 
|             if (existing) { | 
|                 ++existing.allowed; | 
|             } | 
|             else { | 
|                 map.set(value, { allowed: 1, hits: 0 }); | 
|                 patterns.push(EscapeRegex(value)); | 
|             } | 
|         } | 
|         else if (options.once || | 
|             options.only) { | 
|   | 
|             return false; | 
|         } | 
|     } | 
|   | 
|     if (!patterns.length) {                     // Non-empty string contains unlimited empty string | 
|         return true; | 
|     } | 
|   | 
|     // Match patterns | 
|   | 
|     const regex = new RegExp(`(${patterns.join('|')})`, 'g'); | 
|     const leftovers = ref.replace(regex, ($0, $1) => { | 
|   | 
|         ++map.get($1).hits; | 
|         return '';                              // Remove from string | 
|     }); | 
|   | 
|     // Validate results | 
|   | 
|     if (options.only && | 
|         leftovers) { | 
|   | 
|         return false; | 
|     } | 
|   | 
|     let any = false; | 
|     for (const match of map.values()) { | 
|         if (match.hits) { | 
|             any = true; | 
|         } | 
|   | 
|         if (match.hits === match.allowed) { | 
|             continue; | 
|         } | 
|   | 
|         if (match.hits < match.allowed && | 
|             !options.part) { | 
|   | 
|             return false; | 
|         } | 
|   | 
|         // match.hits > match.allowed | 
|   | 
|         if (options.once) { | 
|             return false; | 
|         } | 
|     } | 
|   | 
|     return !!any; | 
| }; | 
|   | 
|   | 
| internals.compare = function (options) { | 
|   | 
|     if (!options.deep) { | 
|         return internals.shallow; | 
|     } | 
|   | 
|     const hasOnly = options.only !== undefined; | 
|     const hasPart = options.part !== undefined; | 
|   | 
|     const flags = { | 
|         prototype: hasOnly ? options.only : hasPart ? !options.part : false, | 
|         part: hasOnly ? !options.only : hasPart ? options.part : false | 
|     }; | 
|   | 
|     return (a, b) => DeepEqual(a, b, flags); | 
| }; | 
|   | 
|   | 
| internals.shallow = function (a, b) { | 
|   | 
|     return a === b; | 
| }; |