| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 | '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;};
 |