zerodep
Version:
Zero dependency utils for various environments.
877 lines • 56.1 kB
JavaScript
'use strict';
// remember: typeof null === 'object';
module.exports = {
// filter array unique elements
uniqueArray: (array) => Array.isArray(array) && [...new Set(array)],
// filter array duplicates
arrayDuplicates: (array) => Array.isArray(array) && array.filter((item, index) => index !== array.indexOf(item)),
// test if string is numeric
isNumeric: (string) => !isNaN(parseFloat(string)) && isFinite(string),
// test if type is object but not array
isObjectNotArray: (object) => object && typeof object === 'object' && !Array.isArray(object),
// test if array and has no object items
isSimpleArray: (array) => Array.isArray(array) && !array.filter((item) => typeof item === 'object').length,
// test if target array includes all the items from the compared array
includesArrayItems: (target, compared) => Array.isArray(target) && Array.isArray(compared) && compared.every((item) => target.includes(item)),
// test if a given context fits in a larger context
isContextMatch: (ctx, match, minMatchKeys = 1) => {
return ctx && typeof ctx === 'object' && match && typeof match === 'object' && Object.keys(match).length >= minMatchKeys && Object.keys(match).reduce((acc, key) => acc && ctx[key] == match[key], true);
},
// test if a given context fits in a larger context including types
isExactContextMatch: (ctx, match, minMatchKeys = 1) => {
return ctx && typeof ctx === 'object' && match && typeof match === 'object' && Object.keys(match).length >= minMatchKeys && Object.keys(match).reduce((acc, key) => acc && ctx[key] === match[key], true);
},
// test if a given context fits in a larger context - any of array items if value - all of array items if array
isInContextMatch: function (ctx, match, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && match && typeof match === 'object' && Object.keys(match).length >= minMatchKeys && Object.keys(match).reduce((acc, key) => acc && (ctx[key] == match[key] || (Array.isArray(match[key]) && match[key].includes(ctx[key])) || this.includesArrayItems(ctx[key], match[key])), true);
},
// test if a given context fits in a larger context including types - any of array items if value - all of array items if array
isInExactContextMatch: function (ctx, match, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && match && typeof match === 'object' && Object.keys(match).length >= minMatchKeys && Object.keys(match).reduce((acc, key) => acc && (ctx[key] === match[key] || (Array.isArray(match[key]) && match[key].includes(ctx[key])) || this.includesArrayItems(ctx[key], match[key])), true);
},
// test if a given contexts array has a fit in a larger context - 1 level deep
hasContextMatch: (ctx, matches, minMatchKeys = 1) => {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && Object.keys(matches).reduce((acc, index) => acc || (typeof matches[index] === 'object' && Object.keys(matches[index]).length >= minMatchKeys && Object.keys(matches[index]).reduce((acc, key) => acc && ctx[key] == matches[index][key], true)), false);
},
// test if a given contexts array has a fit in a larger context including types - 1 level deep
hasExactContextMatch: (ctx, matches, minMatchKeys = 1) => {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && Object.keys(matches).reduce((acc, index) => acc || (typeof matches[index] === 'object' && Object.keys(matches[index]).length >= minMatchKeys && Object.keys(matches[index]).reduce((acc, key) => acc && ctx[key] === matches[index][key], true)), false);
},
// test if a given contexts array has a fit in a larger context - 2 levels deep
hasContextsMatches: function (ctx, matches, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && matches.reduce((acc, item) => (acc ? acc : typeof item === 'object' && Object.keys(item).reduce((acc, key) => acc && typeof item[key] === 'object' && this.isContextMatch(ctx[key], item[key], minMatchKeys), true)), false);
},
// test if a given contexts array has a fit in a larger context including types - 2 levels deep
hasExactContextsMatches: function (ctx, matches, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && matches.reduce((acc, item) => (acc ? acc : typeof item === 'object' && Object.keys(item).reduce((acc, key) => acc && typeof item[key] === 'object' && this.isExactContextMatch(ctx[key], item[key], minMatchKeys), true)), false);
},
// test if a given contexts array has a fit in a larger context - any of array items if value - all of array items if array
hasInContextsMatches: function (ctx, matches, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && matches.reduce((acc, item) => (acc ? acc : typeof item === 'object' && Object.keys(item).reduce((acc, key) => acc && typeof item[key] === 'object' && this.isInContextMatch(ctx[key], item[key], minMatchKeys), true)), false);
},
// test if a given contexts array has a fit in a larger context including types - any of array items if value - all of array items if array
hasInExactContextsMatches: function (ctx, matches, minMatchKeys = 1) {
return ctx && typeof ctx === 'object' && Array.isArray(matches) && matches.reduce((acc, item) => (acc ? acc : typeof item === 'object' && Object.keys(item).reduce((acc, key) => acc && typeof item[key] === 'object' && this.isInExactContextMatch(ctx[key], item[key], minMatchKeys), true)), false);
},
// map a table like data to a tree view, eg. const schema = (obj) => ({ [obj.category]: { [obj.month]: obj.total } });
treeViewArray: (target, parse) => {
if (!Array.isArray(target)) return {};
return Promise.resolve(target.reduce((acc, value) => this.mergeDeep(acc, parse(value)), {}));
},
// test JSON problematic chars from string
hasJsonProblematicChars: (string) => /[\u0000-\u0007\u000B\u000E-\u001F\u007F-\u009F\u2028\u2029]/g.test(string),
// remove JSON problematic chars from string
removeJsonProblematicChars: (string) => string.replace(/[\u0000-\u0007\u000B\u000E-\u001F\u007F-\u009F\u2028\u2029]/g, ''),
// escape JSON problematic chars in string
escapeJsonProblematicChars: (string) => string.replace(/[\u0000-\u0007\u000B\u000E-\u001F\u007F-\u009F\u2028\u2029]/g, (match) => '\\u' + match.charCodeAt(0).toString(16).padStart(4, '0')),
// converts object keys to jsonPointer (old version contained .replace(/\\(.)/g, '$1') )
jsonPointer: (...keys) => keys.map((key) => String(key).replaceAll('~', '~0').replaceAll('/', '~1')).join('/'),
// converts jsonPointer to object keys (old version contained .replace(/[\u0022\u005C\u0000-\u001F]/g, '\\$&') )
jsonPointerKeys: (jsonPointer) => {
return String(jsonPointer)
.split('/')
.map((key) => key.replaceAll('~1', '/').replaceAll('~0', '~'));
},
// split the object reference by corresponding delimiter and pass the keys array using spread operator
/**
*
* @function get
* @param {any} object - The root object from which to retrieve the value.
* @param {...PropertyKey} keys - The sequence of keys to follow in the object.
* @returns {any | undefined} - The value found at the nested path, or `undefined` if any key is missing.
*
*/
get: function (/* object, ...keys */) {
let node = arguments[0];
for (let i = 1; i < arguments.length; ++i) {
if (node == null) return undefined;
node = node[arguments[i]];
if (node === undefined) return undefined;
}
return node;
},
// get: (object, ...keys) => {
// let node = object;
// for (let key of keys) {
// if (node == null) return undefined;
// node = node[key];
// if (node === undefined) return undefined;
// }
// return node;
// },
// set deep object key (the deepest primitive will not be overrided)
set: (value, object, ...keys) => {
if (!keys.length) return (object = value);
const key = keys.pop();
const target = keys.reduce((node, key) => {
try {
if (node[key] !== undefined) return node[key];
if (node[key] === undefined) return (node[key] = {}), node[key];
} catch (e) {
return undefined;
}
}, object);
if (target && typeof target === 'object') return ((target[key] = value) && true) || true;
},
// set deep object key (the deepest primitive will be overrided)
setDeep: (value, object, ...keys) => {
if (!keys.length) return (object = value);
const key = keys.pop();
const target = keys.reduce((node, key) => {
try {
if (node[key] && typeof node[key] === 'object') return node[key];
return (node[key] = {}), node[key];
} catch (e) {
return undefined;
}
}, object);
return ((target[key] = value) && true) || true;
},
// only returns false on frozen object properties, else true
delete: function (object, ...keys) {
if (!keys.length) return true;
const key = keys.pop();
const node = this.get(object, ...keys);
if (node) return (delete node[key] && true) || true;
return true;
},
// returns a static object by embeding the values of the referenced keys
clone: function (object, ...keys) {
const cloneDeep = (object) => {
if (typeof object !== 'object' || object === null) return object;
else if (Array.isArray(object)) return object.map(cloneDeep);
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, cloneDeep(value)]));
};
return cloneDeep(this.get(object, ...keys));
},
// same as clone but object keys are sorted
structuredClone: function (object, ...keys) {
const cloneDeep = (object) => {
if (typeof object !== 'object' || object === null) return object;
else if (Array.isArray(object)) return object.map(cloneDeep);
return Object.fromEntries(
Object.entries(object)
.sort()
.map(([key, value]) => [key, cloneDeep(value)])
);
};
return cloneDeep(this.get(object, ...keys));
},
// returns an array of deep keys not including shallow object keys
deepKeys: function (object) {
let array = [];
Object.keys(object).forEach((key) => {
this.parseDeep((...keys) => {
array.push(...keys);
}, object[key]);
});
array = this.uniqueArray(String(array).split(',')).sort();
array.forEach((value, index) => {
if (this.isNumeric(value)) delete array[index];
});
return array.filter((n) => n);
},
// https://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
// returns merged objects, array keys are not merged instead the last array wins
mergeDeep: function (target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (this.isObjectNotArray(target) && this.isObjectNotArray(source)) {
for (const key in source) {
if (this.isObjectNotArray(source[key]) && !(source[key] instanceof RegExp)) {
if (!target[key]) Object.assign(target, { [key]: {} });
this.mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return this.mergeDeep(target, ...sources);
},
// parser shoud return object or array of objects, null, undefined or nothing
replaceDeep: function (parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).find((key) => {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
delete node[key];
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
parse(...keys);
return true;
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
replaceDeepKey: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).find((key) => {
if (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key))) {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
delete node[key];
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
parse(...keys);
return true;
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
replaceDeepKeyParent: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).find((key) => {
if (keys.length && (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key)))) {
const granParent = keys.slice(0, -1).reduce((node, key) => node[key], object);
const parentKey = String(keys.slice(-1));
let assignments = parser(granParent[parentKey], parentKey, key);
if (assignments instanceof Object) {
delete granParent[parentKey];
if (Array.isArray(granParent)) {
Object.entries(assignments).forEach((entry) => {
if (granParent[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(granParent[entry[0]], entry[1]);
} else {
granParent[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(granParent, ...assignments);
}
parse(...keys.slice(0, -1));
return true;
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
replaceDeepPath: function (pathToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).find((key) => {
if ((typeof pathToParse === 'string' && [...keys, key].join('/').endsWith(pathToParse)) || (pathToParse instanceof RegExp && pathToParse.test([...keys, key].join('/')))) {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
delete node[key];
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
parse(...keys);
return true;
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
assignDeep: function (parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
assignDeepKey: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key))) {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
assignDeepKeyParent: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if (keys.length && (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key)))) {
const granParent = keys.slice(0, -1).reduce((node, key) => node[key], object);
const parentKey = String(keys.slice(-1));
let assignments = parser(granParent[parentKey], parentKey, key);
if (assignments instanceof Object) {
if (Array.isArray(granParent)) {
Object.entries(assignments).forEach((entry) => {
if (granParent[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(granParent[entry[0]], entry[1]);
} else {
granParent[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(granParent, ...assignments);
}
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parser shoud return object or array of objects, null, undefined or nothing
assignDeepPath: function (pathToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if ((typeof pathToParse === 'string' && [...keys, key].join('/').endsWith(pathToParse)) || (pathToParse instanceof RegExp && pathToParse.test([...keys, key].join('/')))) {
let assignments = parser(node[key], key);
if (assignments instanceof Object) {
if (Array.isArray(node)) {
Object.entries(assignments).forEach((entry) => {
if (node[entry[0]] instanceof Object && entry[1] instanceof Object) {
Object.assign(node[entry[0]], entry[1]);
} else {
node[entry[0]] = entry[1];
}
});
} else {
if (!Array.isArray(assignments)) assignments = [assignments];
Object.assign(node, ...assignments);
}
}
}
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// deep parse a given object by a given parser
parseDeep: function (parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
parser(...keys, key) && parse(...keys);
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// deep parse a given object key by a given parser
parseDeepKey: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key))) parser(...keys, key) && parse(...keys);
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// deep parse a given object key's parent by a given parser
parseDeepKeyParent: function (keyToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if (keys.length && (key === keyToParse || (keyToParse instanceof RegExp && keyToParse.test(key)))) parser(...keys) && parse(...keys.slice(0, -1));
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// parse a deep path (string of consecutive keys joined by /)
parseDeepPath: function (pathToParse, parser, object, ...keys) {
const parse = (...keys) => {
const node = keys.reduce((node, key) => node[key], object);
if (node && typeof node === 'object') {
Object.keys(node).forEach((key) => {
if ((typeof pathToParse === 'string' && [...keys, key].join('/').endsWith(pathToParse)) || (pathToParse instanceof RegExp && pathToParse.test([...keys, key].join('/')))) parser(...keys, key) && parse(...keys);
if (node[key] && typeof node[key] === 'object') parse(...keys, key);
});
}
};
parse(...keys);
},
// test a - b objects equality. Comparing objects this way is slow and wrong, use with care.
areEqualObjects: (obj1, obj2) => {
if (!(obj1 instanceof Object && obj2 instanceof Object)) return false;
let structure = (object) =>
Object.entries(object)
.sort()
.map((i) => {
if (i[1] instanceof Object) i[1] = structure(i[1]);
return i;
});
return JSON.stringify(structure(obj1)) === JSON.stringify(structure(obj2));
},
// faster than areEqualObjects
areEqualArrays: (arr1, arr2) => {
if (Array.isArray(arr1) && Array.isArray(arr2)) {
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
}
return false;
},
// escape regExp special characters in order to find them literally
escapeRegex: (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), // $& means the whole matched string
// glob to regex (assuming valid globPattern)
// extended glob nesting rules: ?() +() *() !() @() <= from right every rule can nest the ones from its left
// tip: if a glob pattern converted to regex than back to glob is not the same... probably is not a valid glob
globToRegex: function (globPattern) {
let regexPattern = globPattern;
// extended glob
if (/\?\(|\+\(|\*\(|!\(|@\(/.test(globPattern)) {
regexPattern = regexPattern
.replace(/\?\(([^()]+)\)/g, '{$1}?') // Convert ?(pattern) to {pattern}?
.replace(/\+\(([^()]+)\)/g, '{$1}+') // Convert +(pattern) to {pattern}+
.replace(/\*\(([^()]+)\)/g, '{$1}*') // Convert *(pattern) to {pattern}*
.replace(/!\(([^()]+)\)/g, '{?!$1}') // Convert !(pattern) to {?!pattern}
.replace(/@\(([^()]+)\)\{(\d+..\d*)\}/g, '{$1}{$2}') // Convert @(pattern) to {pattern}{\d+..\d*}
.replace(/@\(([^()]+)\)\{(\d+)\}/g, '{$1}{$2}') // Convert @(pattern) to {pattern}{\d+}
.replace(/@\(([^()]+)\)/g, '{$1}{1}') // Convert @(pattern) to {pattern}{1}
.replace(/\|/g, ','); // Replace pipes with commas
}
regexPattern =
regexPattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape characters with special meaning in regex
.replace(/(\\\*){2,}/g, '.*') // Unescape colapsed multiple wildcard as undefined number of chars
.replace(/(\\\})\\\*/g, '$1*') // Unescape wildcard when follows a closing curly bracket
.replace(/\\\*/g, '[^/]*') // Unescape wildcard as undefined number of chars excluding /
.replace(/(\\\})\\\+/g, '$1+') // Unescape + when follows a closing curly bracket
.replace(/(\\\})\\\?/g, '$1?') // Unescape ? when follows a closing curly bracket
.replace(/(\\\{)\\\?/g, '$1?') // Unescape ? when follows a opening curly bracket
.replace(/\\\?/g, '.') // Unescape ? as single character .
.replace(/\\\[/g, '[') // Unescape group opening bracket [
.replace(/\\\]/g, ']') // Unescape group closing bracket ]
.replace(/\\\{(\d+)\\\.\\\.(\d*)\\\}/g, '{$1,$2}') // Unescape {\d+..\d*} as {\d+,\d*}
.replace(/\\\{(\d+)\\\}/g, '{$1}') // Unescape {\d+} as {\d+}
.replace(/\\\{/g, '(') // Unescape { as opening bracket (
.replace(/\\\}/g, ')') // Unescape { as closing bracket )
.replace(/\([^{)]*(\{|\()|(\}|\))[^}(]*\)|\([^()]*\)/g, (match) => match.replace(/,/g, '|')) + '$'; // Replace commas with pipes inside () and add the end of regex
return regexPattern;
},
// regexToGlob is assuming that regexPattern was created using globToRegex
regexToGlob: function (regexPattern) {
let globPattern = regexPattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape characters with special meaning in regex
.replace(/\\\$$/g, '') // Replace the end of regex with nothing
.replace(/\\\|/g, ',') // Replace pipes with commas
.replace(/(\\\))\\\?/g, '$1?') // Unescape ? when follows a closing bracket
.replace(/(\\\))\\\+/g, '$1+') // Unescape + when follows a closing bracket
.replace(/\\\[\\\^\/\\\]\\\*/g, '*') // Unescape wildcard * when not forward slash before
.replace(/(\\\))\\\*/g, '$1*') // Unescape wildcard * when follows a closing bracket
.replace(/\\\.\\\*/g, '**') // Unescape undefined number of chars .* as double wildcard **
.replace(/\\\\\\\./g, '.') // Unescape . as . when escaped in regex
.replace(/\\\./g, '?') // Unescape single character . as ?
.replace(/\\\]/g, ']') // Unescape group closing bracket ]
.replace(/\\\[/g, '[') // Unescape group opening bracket [
.replace(/\\\)/g, '}') // Unescape ) as closing curly bracket }
.replace(/\\\(/g, '{'); // Unescape ( as opening curly bracket {
// extended glob
if (/\)\*|\)\+|\)\?|\(\?\!|\)\{\d+\}|\)\{\d+,\d*\}/.test(regexPattern)) {
globPattern = globPattern
.replace(/\{([^{}]+)\}\?/g, '?($1)') // Convert {pattern}? to ?(pattern)
.replace(/\{([^{}]+)\}\+/g, '+($1)') // Convert {pattern}+ to +(pattern)
.replace(/\{([^{}]+)\}\*/g, '*($1)') // Convert {pattern}* to *(pattern)
.replace(/\{\\\?\!([^{}]+)\}/g, '!($1)') // Convert {?!pattern} to !(pattern)
.replace(/\{([^{}]+)\}\\\{1\\\}/g, '@($1)') // Convert {pattern}{1} to @(pattern)
.replace(/\{([^{}]+)\}\\\{(\d+)\\\}/g, '@($1){$2}') // Convert {pattern}{\d+} to @(pattern){\d+}
.replace(/\{([^{}]+)\}\\\{(\d+),(\d*)\\\}/g, '@($1){$2..$3}') // Convert {pattern}{\d+,\d*} to @(pattern){\d+..\d*}
.replace(/\([^{)]*(\{|\()|(\}|\))[^}(]*\)|\([^()]*\)/g, (match) => match.replace(/,/g, '|')); // Replace commas with pipes inside ()
}
return globPattern;
},
// string parser
search: (string, regex, parser) => {
do {
var match = regex.exec(string);
if (match != null) parser(match);
} while (match != null);
},
// same as string.matchAll(regex) except it returns array instead of iterator
stringMatches: function (string, regex) {
let array = [];
this.search(string, regex, (match) => array.push(match));
return array;
},
// same as string.match(regex) except it returns group matches only
stringCaptureGroupMatches: function (string, regex) {
let array = [];
this.search(string, regex, (match) => array.push(match.shift() && match.find((value) => value)));
return array;
},
// same as string.match(regex) except it returns group matches first
stringCaptureGroupsOrMatches: function (string, regex) {
let array = [];
this.search(string, regex, (match) => array.push(match.reverse().find((value) => value)));
return array;
},
// the result relative uri reference applied to base will point to targetUriReference
relativeUriReference: (targetUriReference, base = 'schema:/') => {
base = new URL(base);
const uri = new URL(targetUriReference, base);
if (uri.href === base.href) return '';
if (uri.protocol !== base.protocol) return targetUriReference;
if (uri.host !== base.host) return '//' + decodeURI(uri.host + uri.pathname) + uri.search + decodeURI(uri.hash);
const baseParts = base.pathname.split('/');
const uriParts = uri.pathname.split('/');
for (let i = 0; i < uriParts.length; i++) {
if (baseParts[i] !== uriParts[i] && i >= baseParts.length - 1) return decodeURI(uriParts.slice(i).join('/')) + uri.search + decodeURI(uri.hash);
if (baseParts[i] !== uriParts[i]) return '../'.repeat(baseParts.length - i - 1) + decodeURI(uriParts.slice(i).join('/')) + uri.search + decodeURI(uri.hash);
if (i === uriParts.length - 1 && uri.search !== base.search && uri.hash !== base.hash) return uri.search + decodeURI(uri.hash);
if (i === uriParts.length - 1 && uri.search !== base.search) return uri.search;
if (i === uriParts.length - 1 && uri.hash !== base.hash) return decodeURI(uri.hash);
}
return targetUriReference;
},
// custom queryString parser, returns object or array
parseQueryString: function (queryString, asArray) {
let result = asArray ? [] : {};
if (!queryString) return result;
queryString = decodeURIComponent(queryString).replace(/^\?/, '');
let params = queryString.split('&');
if (params.length > 0) {
for (let i = 0; i < params.length; i++) {
let args = params[i].split('=');
if (args[1] !== undefined) {
if (args[1] === 'true') {
result[args[0]] = true;
} else if (args[1] === 'false') {
result[args[0]] = false;
} else if (args[1] === 'null') {
result[args[0]] = null;
} else if (this.isNumeric(args[1])) {
result[args[0]] = parseFloat(args[1]);
} else if (args[1].charAt(0) === '{' || args[1].charAt(0) === '[') {
try {
result[args[0]] = JSON.parse(args[1]);
} catch (e) {
result[args[0]] = 'malformed';
}
} else if (args[1].includes(',')) {
result[args[0]] = args[1].split(',');
result[args[0]].forEach((item, i) => {
if (item === 'true') result[args[0]][i] = true;
if (item === 'false') result[args[0]][i] = false;
if (item === 'null') result[args[0]][i] = null;
if (this.isNumeric(item)) result[args[0]][i] = parseFloat(item);
if (item.charAt(0) === '{' || item.charAt(0) === '[') {
try {
result[args[0]][i] = JSON.parse(item);
} catch (e) {
result[args[0]][i] = 'malformed';
}
}
});
} else {
result[args[0]] = args[1];
}
}
}
}
return result;
},
// custom query stringify
queryStringify: function (object) {
if (typeof object !== 'object' || Array.isArray(object)) return '';
return (
'?' +
Object.keys(object)
.map((key) => (this.isSimpleArray(object[key]) ? key + '=' + object[key].join(',') : typeof object[key] === 'object' ? key + '=' + JSON.stringify(object[key]) : key + '=' + object[key]))
.join('&')
);
},
// merge two queryStrings or object with queryString
// test example:
// const initial = '?empt=&undefined&true=true&false=false&null=null&zero=0&one=1&arr=1,2,3&obj={"foo":2,"deep":{"empty":"","null":null}}';
// const upserts = { empty: '', tru: true, fals: false, nul: null, zer: 0, once: 1, deepArr: [0, null, , 'a', { a: '' }] };
mergeQueryStrings: function (initial, upserts) {
if (typeof initial !== 'string' || Array.isArray(upserts)) return '';
if (typeof upserts === 'string') {
return this.queryStringify(Object.assign(this.parseQueryString(initial), this.parseQueryString(upserts)));
} else if (typeof upserts === 'object') {
return this.queryStringify(Object.assign(this.parseQueryString(initial), upserts));
}
},
// convert string to decimal unicode points
stringToDecimalUnicodePoints: (string) => {
if (typeof string !== 'string') return [];
return Array.from(string).map((char) => char.charCodeAt(0));
},
// convert decimal unicode points to string
decimalUnicodePointsToString: (unicodePoints) => {
if (!Array.isArray(unicodePoints)) return '';
return unicodePoints.map((i) => String.fromCharCode(i)).join('');
},
// reverse string
reverseString: (string) => Array.from(string).reverse().join(''),
// Capitalize
capitalizeFirstLetter: (string) => String.fromCodePoint(string.codePointAt(0)).toUpperCase() + Array.from(string).slice(1).join(''),
// camelCase strings
camelCase: function (...strings) {
let result = strings[0].toLowerCase();
for (let i = 1; i < strings.length; i++) {
result += this.capitalizeFirstLetter(strings[i].toLowerCase());
}
return result;
},
// prefix of two or multiple strings
prefixOf: (...strings) => {
// check args
if (!strings[0] || strings.length === 1) return strings[0] || '';
let i = 0;
// increment i while all strings have the same char at position i
while (strings[0][i] && strings.every((string) => string[i] === strings[0][i])) i++;
return strings[0].substring(0, i);
},
// convert a number represented by a string into [bigint, scale]
toBigIntScaled: (numberString) => {
if (typeof numberString !== 'string') return [0n, 0];
numberString = numberString.trim().toLowerCase();
let [base, exponent = '0'] = numberString.split('e');
exponent = parseInt(exponent);
const sign = base.startsWith('-') ? -1n : 1n;
base = base.replace(/^[-+]/, '');
let [integerPart, fractionalPart = ''] = base.split('.');
integerPart = integerPart.replace(/^0+/, '') || '0';
fractionalPart = fractionalPart.replace(/0+$/, '');
const scale = fractionalPart.length - exponent;
const digits = integerPart + fractionalPart + '0'.repeat(scale > 0 ? 0 : Math.abs(scale));
return [sign * BigInt(digits || '0'), scale > 0 ? scale : 0];
},
// accurate, fast for regular cases, slower for edge cases
isMultipleOf: function (number, divisor) {
let result = this.maybeMultipleOf(number, divisor);
if (result !== undefined) return result;
// if a number requires more significant bits that supported by IEEE754.binary64 then the result cannot be accurate even with this workaround
const [numberBigInt, numberScale] = this.toBigIntScaled(number.toString());
const [divisorBigInt, divisorScale] = this.toBigIntScaled(divisor.toString());
// Normalize to same scale
const factor = 10n ** BigInt(Math.max(numberScale, divisorScale));
const scaledNumber = numberBigInt * (factor / 10n ** BigInt(numberScale));
const scaledDivisor = divisorBigInt * (factor / 10n ** BigInt(divisorScale));
return scaledNumber % scaledDivisor === 0n;
},
// helper of isMultipleOf, kept separate for performance reasons
maybeMultipleOf: (number, divisor) => {
if (typeof number !== 'number' || typeof divisor !== 'number' || !isFinite(number) || !isFinite(divisor) || divisor === 0) return false;
// 1. Always true when the number is zero
if (number === 0) return true;
// 2. Number bellow safe integer and divisor is safe integer
if (Math.abs(number) <= Number.MAX_SAFE_INTEGER && Number.isSafeInteger(divisor)) return number % divisor === 0;
// 3. Quotient close to integer (epsilon tolerance)
if (Math.abs(number) <= Number.MAX_SAFE_INTEGER && Math.abs(divisor) <= Number.MAX_SAFE_INTEGER) {
const quotient = number / divisor;
if (Math.abs(quotient) <= Number.MAX_SAFE_INTEGER) return Math.abs(quotient - Math.round(quotient)) < Math.max(Number.EPSILON * Math.abs(quotient), Number.EPSILON);
}
// 4. Defer to more accurate function
return undefined;
},
// simple sleep in milliseconds (sync)
sleep: (milliseconds) => {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if (new Date().getTime() - start > milliseconds) {
break;
}
}
},
// async equivalent of sleep (less overhead)
delay: async (milliseconds) => await new Promise((resolve) => setTimeout(resolve, milliseconds)),
// time in milliseconds (the same from PHP)
microtime: (getAsFloat) => {
var s, now, multiplier;
if (typeof performance !== 'undefined' && performance.now) {
now = performance.now() / 1000;
multiplier = 1e6; // 1,000,000 for microseconds
} else {
now = Date.now ? Date.now() / 1000 : Math.floor(new Date().getTime() / 1000.0);
multiplier = 1e3; // 1,000
}
// Getting microtime as a float is easy
if (getAsFloat) return now;
// Dirty trick to only get the integer part
s = now | 0;
return Math.round((now - s) * multiplier) / multiplier + ' ' + s;
},
// generate UUID v4
generateUUID: () => {
// Public Domain/MIT
var d = new Date().getTime(); //Timestamp
var d2 = typeof performance !== 'undefined' && typeof performance.now !== 'undefined' ? performance.now() * 1000 : 0; //Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16; //random number between 0 and 16
if (d > 0) {
//Use timestamp until depleted
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {
//Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
},
// crc32
crc32: (r) => {
let a;
let o = [];
let n = -1;
for (let c = 0; c < 256; c++) {
a = c;
for (let f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;
o[c] = a;
}
for (let t = 0; t < r.length; t++) n = (n >>> 8) ^ o[255 & (n ^ r.charCodeAt(t))];
return (-1 ^ n) >>> 0;
},
// crc32c
crc32c: (crc, bytes) => {
const POLY = 0x82f63b78;
crc ^= 0xffffffff;
for (let n = 0; n < bytes.length; n++) {
crc ^= bytes[n];
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
crc = crc & 1 ? (crc >>> 1) ^ POLY : crc >>> 1;
}
return crc ^ 0xffffffff;
},
// almost as fast as crypto.createHash('md5').update(JSON.stringify(value)).digest('hex'), but lib free
crc32DuplexHash: function (value) {
const string = JSON.stringify(value);
return this.crc32(string).toString(16) + this.crc32(this.reverseString(string)).toString(16);
},
// base16 encode
base16: (string) => {
if (typeof string !== 'string') throw new TypeError('base16 encoding input must be a string');
let encoded = '';
for (let i = 0; i < string.length; i++) encoded += string.charCodeAt(i).toString(16).toUpperCase().padStart(2, '0');
return encoded;
},
// base16 decode
decodeBase16: (encoded) => {
if (typeof encoded !== 'string') throw new TypeError('base16 encoded input must be a string');
if (!/^[0-9A-F]*$/.test(encoded)) throw new SyntaxError('invalid characters in base16 encoded input');
if (encoded.length % 2 !== 0) throw new SyntaxError('invalid base16 encoded input length');
const bytes = new Uint8Array(encoded.length / 2);
for (let i = 0; i < encoded.length; i += 2) bytes[i / 2] = parseInt(encoded.slice(i, i + 2), 16);
bytes.toString = function () {
let string = null;
try {
string = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (e) {}
return string;
};
return bytes;
},
// base32 encode
base32: (string) => {
if (typeof string !== 'string') throw new TypeError('base32 encoding input must be a string');
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const bytes = typeof window !== 'undefined' ? new TextEncoder().encode(string) : Buffer.from(string, 'utf8');
let bits = '';
let result = '';
for (const byte of bytes) bits += byte.toString(2).padStart(8, '0');
for (let i = 0; i < bits.length; i += 5) re