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