You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					687 lines
				
				17 KiB
			
		
		
			
		
	
	
					687 lines
				
				17 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | // Load modules
 | ||
|  | 
 | ||
|  | const Assert = require('assert'); | ||
|  | const Crypto = require('crypto'); | ||
|  | const Path = require('path'); | ||
|  | 
 | ||
|  | const DeepEqual = require('./deep-equal'); | ||
|  | const Escape = require('./escape'); | ||
|  | 
 | ||
|  | 
 | ||
|  | // Declare internals
 | ||
|  | 
 | ||
|  | const internals = {}; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Deep object or array comparison
 | ||
|  | 
 | ||
|  | exports.deepEqual = DeepEqual; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Clone object or array
 | ||
|  | 
 | ||
|  | exports.clone = function (obj, options = {}, _seen = null) { | ||
|  | 
 | ||
|  |     if (typeof obj !== 'object' || | ||
|  |         obj === null) { | ||
|  | 
 | ||
|  |         return obj; | ||
|  |     } | ||
|  | 
 | ||
|  |     const seen = _seen || new Map(); | ||
|  | 
 | ||
|  |     const lookup = seen.get(obj); | ||
|  |     if (lookup) { | ||
|  |         return lookup; | ||
|  |     } | ||
|  | 
 | ||
|  |     let newObj; | ||
|  |     let cloneDeep = false; | ||
|  |     const isArray = Array.isArray(obj); | ||
|  | 
 | ||
|  |     if (!isArray) { | ||
|  |         if (Buffer.isBuffer(obj)) { | ||
|  |             newObj = Buffer.from(obj); | ||
|  |         } | ||
|  |         else if (obj instanceof Date) { | ||
|  |             newObj = new Date(obj.getTime()); | ||
|  |         } | ||
|  |         else if (obj instanceof RegExp) { | ||
|  |             newObj = new RegExp(obj); | ||
|  |         } | ||
|  |         else { | ||
|  |             if (options.prototype !== false) {          // Defaults to true
 | ||
|  |                 const proto = Object.getPrototypeOf(obj); | ||
|  |                 if (proto && | ||
|  |                     proto.isImmutable) { | ||
|  | 
 | ||
|  |                     newObj = obj; | ||
|  |                 } | ||
|  |                 else { | ||
|  |                     newObj = Object.create(proto); | ||
|  |                     cloneDeep = true; | ||
|  |                 } | ||
|  |             } | ||
|  |             else { | ||
|  |                 newObj = {}; | ||
|  |                 cloneDeep = true; | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  |     else { | ||
|  |         newObj = []; | ||
|  |         cloneDeep = true; | ||
|  |     } | ||
|  | 
 | ||
|  |     seen.set(obj, newObj); | ||
|  | 
 | ||
|  |     if (cloneDeep) { | ||
|  |         const keys = internals.keys(obj, options); | ||
|  |         for (let i = 0; i < keys.length; ++i) { | ||
|  |             const key = keys[i]; | ||
|  | 
 | ||
|  |             if (isArray && key === 'length') { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             const descriptor = Object.getOwnPropertyDescriptor(obj, key); | ||
|  |             if (descriptor && | ||
|  |                 (descriptor.get || | ||
|  |                     descriptor.set)) { | ||
|  | 
 | ||
|  |                 Object.defineProperty(newObj, key, descriptor); | ||
|  |             } | ||
|  |             else { | ||
|  |                 Object.defineProperty(newObj, key, { | ||
|  |                     enumerable: descriptor ? descriptor.enumerable : true, | ||
|  |                     writable: true, | ||
|  |                     configurable: true, | ||
|  |                     value: exports.clone(obj[key], options, seen) | ||
|  |                 }); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         if (isArray) { | ||
|  |             newObj.length = obj.length; | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return newObj; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | internals.keys = function (obj, options = {}) { | ||
|  | 
 | ||
|  |     return options.symbols ? Reflect.ownKeys(obj) : Object.getOwnPropertyNames(obj); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied
 | ||
|  | 
 | ||
|  | exports.merge = function (target, source, isNullOverride /* = true */, isMergeArrays /* = true */) { | ||
|  | 
 | ||
|  |     exports.assert(target && typeof target === 'object', 'Invalid target value: must be an object'); | ||
|  |     exports.assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object'); | ||
|  | 
 | ||
|  |     if (!source) { | ||
|  |         return target; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (Array.isArray(source)) { | ||
|  |         exports.assert(Array.isArray(target), 'Cannot merge array onto an object'); | ||
|  |         if (isMergeArrays === false) {                                                  // isMergeArrays defaults to true
 | ||
|  |             target.length = 0;                                                          // Must not change target assignment
 | ||
|  |         } | ||
|  | 
 | ||
|  |         for (let i = 0; i < source.length; ++i) { | ||
|  |             target.push(exports.clone(source[i])); | ||
|  |         } | ||
|  | 
 | ||
|  |         return target; | ||
|  |     } | ||
|  | 
 | ||
|  |     const keys = internals.keys(source); | ||
|  |     for (let i = 0; i < keys.length; ++i) { | ||
|  |         const key = keys[i]; | ||
|  |         if (key === '__proto__' || | ||
|  |             !Object.prototype.propertyIsEnumerable.call(source, key)) { | ||
|  | 
 | ||
|  |             continue; | ||
|  |         } | ||
|  | 
 | ||
|  |         const value = source[key]; | ||
|  |         if (value && | ||
|  |             typeof value === 'object') { | ||
|  | 
 | ||
|  |             if (!target[key] || | ||
|  |                 typeof target[key] !== 'object' || | ||
|  |                 (Array.isArray(target[key]) !== Array.isArray(value)) || | ||
|  |                 value instanceof Date || | ||
|  |                 Buffer.isBuffer(value) || | ||
|  |                 value instanceof RegExp) { | ||
|  | 
 | ||
|  |                 target[key] = exports.clone(value); | ||
|  |             } | ||
|  |             else { | ||
|  |                 exports.merge(target[key], value, isNullOverride, isMergeArrays); | ||
|  |             } | ||
|  |         } | ||
|  |         else { | ||
|  |             if (value !== null && | ||
|  |                 value !== undefined) {                              // Explicit to preserve empty strings
 | ||
|  | 
 | ||
|  |                 target[key] = value; | ||
|  |             } | ||
|  |             else if (isNullOverride !== false) {                    // Defaults to true
 | ||
|  |                 target[key] = value; | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return target; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Apply options to a copy of the defaults
 | ||
|  | 
 | ||
|  | exports.applyToDefaults = function (defaults, options, isNullOverride) { | ||
|  | 
 | ||
|  |     exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object'); | ||
|  |     exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object'); | ||
|  | 
 | ||
|  |     if (!options) {                                                 // If no options, return null
 | ||
|  |         return null; | ||
|  |     } | ||
|  | 
 | ||
|  |     const copy = exports.clone(defaults); | ||
|  | 
 | ||
|  |     if (options === true) {                                         // If options is set to true, use defaults
 | ||
|  |         return copy; | ||
|  |     } | ||
|  | 
 | ||
|  |     return exports.merge(copy, options, isNullOverride === true, false); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Clone an object except for the listed keys which are shallow copied
 | ||
|  | 
 | ||
|  | exports.cloneWithShallow = function (source, keys, options) { | ||
|  | 
 | ||
|  |     if (!source || | ||
|  |         typeof source !== 'object') { | ||
|  | 
 | ||
|  |         return source; | ||
|  |     } | ||
|  | 
 | ||
|  |     const storage = internals.store(source, keys);    // Move shallow copy items to storage
 | ||
|  |     const copy = exports.clone(source, options);      // Deep copy the rest
 | ||
|  |     internals.restore(copy, source, storage);         // Shallow copy the stored items and restore
 | ||
|  |     return copy; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | internals.store = function (source, keys) { | ||
|  | 
 | ||
|  |     const storage = new Map(); | ||
|  |     for (let i = 0; i < keys.length; ++i) { | ||
|  |         const key = keys[i]; | ||
|  |         const value = exports.reach(source, key); | ||
|  |         if (typeof value === 'object' || | ||
|  |             typeof value === 'function') { | ||
|  | 
 | ||
|  |             storage.set(key, value); | ||
|  |             internals.reachSet(source, key, undefined); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return storage; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | internals.restore = function (copy, source, storage) { | ||
|  | 
 | ||
|  |     for (const [key, value] of storage) { | ||
|  |         internals.reachSet(copy, key, value); | ||
|  |         internals.reachSet(source, key, value); | ||
|  |     } | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | internals.reachSet = function (obj, key, value) { | ||
|  | 
 | ||
|  |     const path = Array.isArray(key) ? key : key.split('.'); | ||
|  |     let ref = obj; | ||
|  |     for (let i = 0; i < path.length; ++i) { | ||
|  |         const segment = path[i]; | ||
|  |         if (i + 1 === path.length) { | ||
|  |             ref[segment] = value; | ||
|  |         } | ||
|  | 
 | ||
|  |         ref = ref[segment]; | ||
|  |     } | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Apply options to defaults except for the listed keys which are shallow copied from option without merging
 | ||
|  | 
 | ||
|  | exports.applyToDefaultsWithShallow = function (defaults, options, keys) { | ||
|  | 
 | ||
|  |     exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object'); | ||
|  |     exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object'); | ||
|  |     exports.assert(keys && Array.isArray(keys), 'Invalid keys'); | ||
|  | 
 | ||
|  |     if (!options) {                                                 // If no options, return null
 | ||
|  |         return null; | ||
|  |     } | ||
|  | 
 | ||
|  |     const copy = exports.cloneWithShallow(defaults, keys); | ||
|  | 
 | ||
|  |     if (options === true) {                                         // If options is set to true, use defaults
 | ||
|  |         return copy; | ||
|  |     } | ||
|  | 
 | ||
|  |     const storage = internals.store(options, keys);     // Move shallow copy items to storage
 | ||
|  |     exports.merge(copy, options, false, false);         // Deep copy the rest
 | ||
|  |     internals.restore(copy, options, storage);          // Shallow copy the stored items and restore
 | ||
|  |     return copy; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Find the common unique items in two arrays
 | ||
|  | 
 | ||
|  | exports.intersect = function (array1, array2, justFirst) { | ||
|  | 
 | ||
|  |     if (!array1 || | ||
|  |         !array2) { | ||
|  | 
 | ||
|  |         return (justFirst ? null : []); | ||
|  |     } | ||
|  | 
 | ||
|  |     const common = []; | ||
|  |     const hash = (Array.isArray(array1) ? new Set(array1) : array1); | ||
|  |     const found = new Set(); | ||
|  |     for (const value of array2) { | ||
|  |         if (internals.has(hash, value) && | ||
|  |             !found.has(value)) { | ||
|  | 
 | ||
|  |             if (justFirst) { | ||
|  |                 return value; | ||
|  |             } | ||
|  | 
 | ||
|  |             common.push(value); | ||
|  |             found.add(value); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return (justFirst ? null : common); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | internals.has = function (ref, key) { | ||
|  | 
 | ||
|  |     if (typeof ref.has === 'function') { | ||
|  |         return ref.has(key); | ||
|  |     } | ||
|  | 
 | ||
|  |     return ref[key] !== undefined; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Test if the reference contains the values
 | ||
|  | 
 | ||
|  | exports.contain = 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); | ||
|  |     } | ||
|  | 
 | ||
|  |     exports.assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object'); | ||
|  |     exports.assert(values.length, 'Values array cannot be empty'); | ||
|  | 
 | ||
|  |     let compare; | ||
|  |     let compareFlags; | ||
|  |     if (options.deep) { | ||
|  |         compare = exports.deepEqual; | ||
|  | 
 | ||
|  |         const hasOnly = options.hasOwnProperty('only'); | ||
|  |         const hasPart = options.hasOwnProperty('part'); | ||
|  | 
 | ||
|  |         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') { | ||
|  |         let pattern = '('; | ||
|  |         for (let i = 0; i < values.length; ++i) { | ||
|  |             const value = values[i]; | ||
|  |             exports.assert(typeof value === 'string', 'Cannot compare string reference to non-string value'); | ||
|  |             pattern += (i ? '|' : '') + exports.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)) { | ||
|  |         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 = internals.keys(ref, options); | ||
|  |         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; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Flatten array
 | ||
|  | 
 | ||
|  | exports.flatten = function (array, target) { | ||
|  | 
 | ||
|  |     const result = target || []; | ||
|  | 
 | ||
|  |     for (let i = 0; i < array.length; ++i) { | ||
|  |         if (Array.isArray(array[i])) { | ||
|  |             exports.flatten(array[i], result); | ||
|  |         } | ||
|  |         else { | ||
|  |             result.push(array[i]); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return result; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Convert an object key chain string ('a.b.c') to reference (object[a][b][c])
 | ||
|  | 
 | ||
|  | exports.reach = function (obj, chain, options) { | ||
|  | 
 | ||
|  |     if (chain === false || | ||
|  |         chain === null || | ||
|  |         typeof chain === 'undefined') { | ||
|  | 
 | ||
|  |         return obj; | ||
|  |     } | ||
|  | 
 | ||
|  |     options = options || {}; | ||
|  |     if (typeof options === 'string') { | ||
|  |         options = { separator: options }; | ||
|  |     } | ||
|  | 
 | ||
|  |     const isChainArray = Array.isArray(chain); | ||
|  | 
 | ||
|  |     exports.assert(!isChainArray || !options.separator, 'Separator option no valid for array-based chain'); | ||
|  | 
 | ||
|  |     const path = isChainArray ? chain : chain.split(options.separator || '.'); | ||
|  |     let ref = obj; | ||
|  |     for (let i = 0; i < path.length; ++i) { | ||
|  |         let key = path[i]; | ||
|  | 
 | ||
|  |         if (Array.isArray(ref)) { | ||
|  |             const number = Number(key); | ||
|  | 
 | ||
|  |             if (Number.isInteger(number) && number < 0) { | ||
|  |                 key = ref.length + number; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         if (!ref || | ||
|  |             !((typeof ref === 'object' || typeof ref === 'function') && key in ref) || | ||
|  |             (typeof ref !== 'object' && options.functions === false)) {         // Only object and function can have properties
 | ||
|  | 
 | ||
|  |             exports.assert(!options.strict || i + 1 === path.length, 'Missing segment', key, 'in reach path ', chain); | ||
|  |             exports.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', 'Invalid segment', key, 'in reach path ', chain); | ||
|  |             ref = options.default; | ||
|  |             break; | ||
|  |         } | ||
|  | 
 | ||
|  |         ref = ref[key]; | ||
|  |     } | ||
|  | 
 | ||
|  |     return ref; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.reachTemplate = function (obj, template, options) { | ||
|  | 
 | ||
|  |     return template.replace(/{([^}]+)}/g, ($0, chain) => { | ||
|  | 
 | ||
|  |         const value = exports.reach(obj, chain, options); | ||
|  |         return (value === undefined || value === null ? '' : value); | ||
|  |     }); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.assert = function (condition, ...args) { | ||
|  | 
 | ||
|  |     if (condition) { | ||
|  |         return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (args.length === 1 && args[0] instanceof Error) { | ||
|  |         throw args[0]; | ||
|  |     } | ||
|  | 
 | ||
|  |     const msgs = args | ||
|  |         .filter((arg) => arg !== '') | ||
|  |         .map((arg) => { | ||
|  | 
 | ||
|  |             return typeof arg === 'string' ? arg : arg instanceof Error ? arg.message : exports.stringify(arg); | ||
|  |         }); | ||
|  | 
 | ||
|  |     throw new Assert.AssertionError({ | ||
|  |         message: msgs.join(' ') || 'Unknown error', | ||
|  |         actual: false, | ||
|  |         expected: true, | ||
|  |         operator: '==', | ||
|  |         stackStartFunction: exports.assert | ||
|  |     }); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.Bench = function () { | ||
|  | 
 | ||
|  |     this.ts = 0; | ||
|  |     this.reset(); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.Bench.prototype.reset = function () { | ||
|  | 
 | ||
|  |     this.ts = exports.Bench.now(); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.Bench.prototype.elapsed = function () { | ||
|  | 
 | ||
|  |     return exports.Bench.now() - this.ts; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.Bench.now = function () { | ||
|  | 
 | ||
|  |     const ts = process.hrtime(); | ||
|  |     return (ts[0] * 1e3) + (ts[1] / 1e6); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Escape string for Regex construction
 | ||
|  | 
 | ||
|  | exports.escapeRegex = function (string) { | ||
|  | 
 | ||
|  |     // Escape ^$.*+-?=!:|\/()[]{},
 | ||
|  |     return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&'); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Escape attribute value for use in HTTP header
 | ||
|  | 
 | ||
|  | exports.escapeHeaderAttribute = function (attribute) { | ||
|  | 
 | ||
|  |     // Allowed value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "
 | ||
|  | 
 | ||
|  |     exports.assert(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$/.test(attribute), 'Bad attribute value (' + attribute + ')'); | ||
|  | 
 | ||
|  |     return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');                             // Escape quotes and slash
 | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.escapeHtml = function (string) { | ||
|  | 
 | ||
|  |     return Escape.escapeHtml(string); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.escapeJson = function (string) { | ||
|  | 
 | ||
|  |     return Escape.escapeJson(string); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.once = function (method) { | ||
|  | 
 | ||
|  |     if (method._hoekOnce) { | ||
|  |         return method; | ||
|  |     } | ||
|  | 
 | ||
|  |     let once = false; | ||
|  |     const wrapped = function (...args) { | ||
|  | 
 | ||
|  |         if (!once) { | ||
|  |             once = true; | ||
|  |             method(...args); | ||
|  |         } | ||
|  |     }; | ||
|  | 
 | ||
|  |     wrapped._hoekOnce = true; | ||
|  |     return wrapped; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.ignore = function () { }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.uniqueFilename = function (path, extension) { | ||
|  | 
 | ||
|  |     if (extension) { | ||
|  |         extension = extension[0] !== '.' ? '.' + extension : extension; | ||
|  |     } | ||
|  |     else { | ||
|  |         extension = ''; | ||
|  |     } | ||
|  | 
 | ||
|  |     path = Path.resolve(path); | ||
|  |     const name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-') + extension; | ||
|  |     return Path.join(path, name); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.stringify = function (...args) { | ||
|  | 
 | ||
|  |     try { | ||
|  |         return JSON.stringify.apply(null, args); | ||
|  |     } | ||
|  |     catch (err) { | ||
|  |         return '[Cannot display object: ' + err.message + ']'; | ||
|  |     } | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.wait = function (timeout) { | ||
|  | 
 | ||
|  |     return new Promise((resolve) => setTimeout(resolve, timeout)); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | exports.block = function () { | ||
|  | 
 | ||
|  |     return new Promise(exports.ignore); | ||
|  | }; |