@augment-vir/common
Version:
A collection of augments, helpers types, functions, and classes for any JavaScript environment.
601 lines (600 loc) • 25.2 kB
JavaScript
/* node:coverage disable */
/**
* This file is copied from the
* [safe-stable-stringify](https://npmjs.com/package/safe-stable-stringify) package, and modified so
* that it can actually be imported, at:
* https://github.com/BridgeAR/safe-stable-stringify/blob/8a02137ac933eff57dd6e49beb9ee766fe8dd372/index.js
*
* It has the following license:
*
* The MIT License (MIT)
*
* Copyright (c) Ruben Bridgewater
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* eslint-disable */
// @ts-nocheck
const { hasOwnProperty } = Object.prototype;
const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/;
// Escape C0 control characters, double quotes, the backslash and every code
// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF.
function strEscape(str) {
// Some magic numbers that worked out fine while benchmarking with v8 8.0
if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) {
return `"${str}"`;
}
return JSON.stringify(str);
}
function sort(array, comparator) {
// Insertion sort is very efficient for small input sizes, but it has a bad
// worst case complexity. Thus, use native array sort for bigger values.
if (array.length > 2e2 || comparator) {
return array.sort(comparator);
}
for (let i = 1; i < array.length; i++) {
const currentValue = array[i];
let position = i;
while (position !== 0 && array[position - 1] > currentValue) {
array[position] = array[position - 1];
position--;
}
array[position] = currentValue;
}
return array;
}
const typedArrayPrototypeGetSymbolToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(new Int8Array())), Symbol.toStringTag).get;
function isTypedArrayWithEntries(value) {
return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0;
}
function stringifyTypedArray(array, separator, maximumBreadth) {
if (array.length < maximumBreadth) {
maximumBreadth = array.length;
}
const whitespace = separator === ',' ? '' : ' ';
let res = `"0":${whitespace}${array[0]}`;
for (let i = 1; i < maximumBreadth; i++) {
res += `${separator}"${i}":${whitespace}${array[i]}`;
}
return res;
}
function getCircularValueOption(options) {
if (hasOwnProperty.call(options, 'circularValue')) {
const circularValue = options.circularValue;
if (typeof circularValue === 'string') {
return `"${circularValue}"`;
}
if (circularValue == null) {
return circularValue;
}
if (circularValue === Error || circularValue === TypeError) {
return {
toString() {
throw new TypeError('Converting circular structure to JSON');
},
};
}
throw new TypeError('The "circularValue" argument must be of type string or the value null or undefined');
}
return '"[Circular]"';
}
function getDeterministicOption(options) {
let value;
if (hasOwnProperty.call(options, 'deterministic')) {
value = options.deterministic;
if (typeof value !== 'boolean' && typeof value !== 'function') {
throw new TypeError('The "deterministic" argument must be of type boolean or comparator function');
}
}
return value === undefined ? true : value;
}
function getBooleanOption(options, key) {
let value;
if (hasOwnProperty.call(options, key)) {
value = options[key];
if (typeof value !== 'boolean') {
throw new TypeError(`The "${key}" argument must be of type boolean`);
}
}
return value === undefined ? true : value;
}
function getPositiveIntegerOption(options, key) {
let value;
if (hasOwnProperty.call(options, key)) {
value = options[key];
if (typeof value !== 'number') {
throw new TypeError(`The "${key}" argument must be of type number`);
}
if (!Number.isInteger(value)) {
throw new TypeError(`The "${key}" argument must be an integer`);
}
if (value < 1) {
throw new RangeError(`The "${key}" argument must be >= 1`);
}
}
return value === undefined ? Infinity : value;
}
function getItemCount(number) {
if (number === 1) {
return '1 item';
}
return `${number} items`;
}
function getUniqueReplacerSet(replacerArray) {
const replacerSet = new Set();
for (const value of replacerArray) {
if (typeof value === 'string' || typeof value === 'number') {
replacerSet.add(String(value));
}
}
return replacerSet;
}
function getStrictOption(options) {
if (hasOwnProperty.call(options, 'strict')) {
const value = options.strict;
if (typeof value !== 'boolean') {
throw new TypeError('The "strict" argument must be of type boolean');
}
if (value) {
return (value) => {
let message = `Object can not safely be stringified. Received type ${typeof value}`;
if (typeof value !== 'function') {
message += ` (${value.toString()})`;
}
throw new Error(message);
};
}
}
}
export function configure(options) {
options = { ...options };
const fail = getStrictOption(options);
if (fail) {
if (options.bigint === undefined) {
options.bigint = false;
}
if (!('circularValue' in options)) {
options.circularValue = Error;
}
}
const circularValue = getCircularValueOption(options);
const bigint = getBooleanOption(options, 'bigint');
const deterministic = getDeterministicOption(options);
const comparator = typeof deterministic === 'function' ? deterministic : undefined;
const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth');
const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth');
function stringifyFnReplacer(key, parent, stack, replacer, spacer, indentation) {
let value = parent[key];
if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
value = replacer.call(parent, key, value);
switch (typeof value) {
case 'string':
return strEscape(value);
case 'object': {
if (value === null) {
return 'null';
}
if (stack.includes(value)) {
return circularValue;
}
let res = '';
let join = ',';
const originalIndentation = indentation;
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
if (maximumDepth < stack.length + 1) {
return '"[Array]"';
}
stack.push(value);
if (spacer !== '') {
indentation += spacer;
res += `\n${indentation}`;
join = `,\n${indentation}`;
}
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
let i = 0;
for (; i < maximumValuesToStringify - 1; i++) {
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
res += join;
}
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
if (value.length - 1 > maximumBreadth) {
const removedKeys = value.length - maximumBreadth - 1;
res += `${join}"... ${getItemCount(removedKeys)} not stringified"`;
}
if (spacer !== '') {
res += `\n${originalIndentation}`;
}
stack.pop();
return `[${res}]`;
}
let keys = Object.keys(value);
const keyLength = keys.length;
if (keyLength === 0) {
return '{}';
}
if (maximumDepth < stack.length + 1) {
return '"[Object]"';
}
let whitespace = '';
let separator = '';
if (spacer !== '') {
indentation += spacer;
join = `,\n${indentation}`;
whitespace = ' ';
}
const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
if (deterministic && !isTypedArrayWithEntries(value)) {
keys = sort(keys, comparator);
}
stack.push(value);
for (let i = 0; i < maximumPropertiesToStringify; i++) {
const key = keys[i];
const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation);
if (tmp !== undefined) {
res += `${separator}${strEscape(key)}:${whitespace}${tmp}`;
separator = join;
}
}
if (keyLength > maximumBreadth) {
const removedKeys = keyLength - maximumBreadth;
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
separator = join;
}
if (spacer !== '' && separator.length > 1) {
res = `\n${indentation}${res}\n${originalIndentation}`;
}
stack.pop();
return `{${res}}`;
}
case 'number':
return isFinite(value) ? String(value) : fail ? fail(value) : 'null';
case 'boolean':
return value ? 'true' : 'false';
case 'undefined':
return undefined;
case 'bigint':
if (bigint) {
return String(value);
}
// fallthrough
default:
return fail ? fail(value) : undefined;
}
}
function stringifyArrayReplacer(key, value, stack, replacer, spacer, indentation) {
if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
switch (typeof value) {
case 'string':
return strEscape(value);
case 'object': {
if (value === null) {
return 'null';
}
if (stack.includes(value)) {
return circularValue;
}
const originalIndentation = indentation;
let res = '';
let join = ',';
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
if (maximumDepth < stack.length + 1) {
return '"[Array]"';
}
stack.push(value);
if (spacer !== '') {
indentation += spacer;
res += `\n${indentation}`;
join = `,\n${indentation}`;
}
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
let i = 0;
for (; i < maximumValuesToStringify - 1; i++) {
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
res += join;
}
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
if (value.length - 1 > maximumBreadth) {
const removedKeys = value.length - maximumBreadth - 1;
res += `${join}"... ${getItemCount(removedKeys)} not stringified"`;
}
if (spacer !== '') {
res += `\n${originalIndentation}`;
}
stack.pop();
return `[${res}]`;
}
stack.push(value);
let whitespace = '';
if (spacer !== '') {
indentation += spacer;
join = `,\n${indentation}`;
whitespace = ' ';
}
let separator = '';
for (const key of replacer) {
const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation);
if (tmp !== undefined) {
res += `${separator}${strEscape(key)}:${whitespace}${tmp}`;
separator = join;
}
}
if (spacer !== '' && separator.length > 1) {
res = `\n${indentation}${res}\n${originalIndentation}`;
}
stack.pop();
return `{${res}}`;
}
case 'number':
return isFinite(value) ? String(value) : fail ? fail(value) : 'null';
case 'boolean':
return value ? 'true' : 'false';
case 'undefined':
return undefined;
case 'bigint':
if (bigint) {
return String(value);
}
// fallthrough
default:
return fail ? fail(value) : undefined;
}
}
function stringifyIndent(key, value, stack, spacer, indentation) {
switch (typeof value) {
case 'string':
return strEscape(value);
case 'object': {
if (value === null) {
return 'null';
}
if (typeof value.toJSON === 'function') {
value = value.toJSON(key);
// Prevent calling `toJSON` again.
if (typeof value !== 'object') {
return stringifyIndent(key, value, stack, spacer, indentation);
}
if (value === null) {
return 'null';
}
}
if (stack.includes(value)) {
return circularValue;
}
const originalIndentation = indentation;
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
if (maximumDepth < stack.length + 1) {
return '"[Array]"';
}
stack.push(value);
indentation += spacer;
let res = `\n${indentation}`;
const join = `,\n${indentation}`;
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
let i = 0;
for (; i < maximumValuesToStringify - 1; i++) {
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
res += join;
}
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
res += tmp === undefined ? 'null' : tmp;
if (value.length - 1 > maximumBreadth) {
const removedKeys = value.length - maximumBreadth - 1;
res += `${join}"... ${getItemCount(removedKeys)} not stringified"`;
}
res += `\n${originalIndentation}`;
stack.pop();
return `[${res}]`;
}
let keys = Object.keys(value);
const keyLength = keys.length;
if (keyLength === 0) {
return '{}';
}
if (maximumDepth < stack.length + 1) {
return '"[Object]"';
}
indentation += spacer;
const join = `,\n${indentation}`;
let res = '';
let separator = '';
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
if (isTypedArrayWithEntries(value)) {
res += stringifyTypedArray(value, join, maximumBreadth);
keys = keys.slice(value.length);
maximumPropertiesToStringify -= value.length;
separator = join;
}
if (deterministic) {
keys = sort(keys, comparator);
}
stack.push(value);
for (let i = 0; i < maximumPropertiesToStringify; i++) {
const key = keys[i];
const tmp = stringifyIndent(key, value[key], stack, spacer, indentation);
if (tmp !== undefined) {
res += `${separator}${strEscape(key)}: ${tmp}`;
separator = join;
}
}
if (keyLength > maximumBreadth) {
const removedKeys = keyLength - maximumBreadth;
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
separator = join;
}
if (separator !== '') {
res = `\n${indentation}${res}\n${originalIndentation}`;
}
stack.pop();
return `{${res}}`;
}
case 'number':
return isFinite(value) ? String(value) : fail ? fail(value) : 'null';
case 'boolean':
return value ? 'true' : 'false';
case 'undefined':
return undefined;
case 'bigint':
if (bigint) {
return String(value);
}
// fallthrough
default:
return fail ? fail(value) : undefined;
}
}
function stringifySimple(key, value, stack) {
switch (typeof value) {
case 'string':
return strEscape(value);
case 'object': {
if (value === null) {
return 'null';
}
if (typeof value.toJSON === 'function') {
value = value.toJSON(key);
// Prevent calling `toJSON` again
if (typeof value !== 'object') {
return stringifySimple(key, value, stack);
}
if (value === null) {
return 'null';
}
}
if (stack.includes(value)) {
return circularValue;
}
let res = '';
const hasLength = value.length !== undefined;
if (hasLength && Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
if (maximumDepth < stack.length + 1) {
return '"[Array]"';
}
stack.push(value);
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
let i = 0;
for (; i < maximumValuesToStringify - 1; i++) {
const tmp = stringifySimple(String(i), value[i], stack);
res += tmp === undefined ? 'null' : tmp;
res += ',';
}
const tmp = stringifySimple(String(i), value[i], stack);
res += tmp === undefined ? 'null' : tmp;
if (value.length - 1 > maximumBreadth) {
const removedKeys = value.length - maximumBreadth - 1;
res += `,"... ${getItemCount(removedKeys)} not stringified"`;
}
stack.pop();
return `[${res}]`;
}
let keys = Object.keys(value);
const keyLength = keys.length;
if (keyLength === 0) {
return '{}';
}
if (maximumDepth < stack.length + 1) {
return '"[Object]"';
}
let separator = '';
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
if (hasLength && isTypedArrayWithEntries(value)) {
res += stringifyTypedArray(value, ',', maximumBreadth);
keys = keys.slice(value.length);
maximumPropertiesToStringify -= value.length;
separator = ',';
}
if (deterministic) {
keys = sort(keys, comparator);
}
stack.push(value);
for (let i = 0; i < maximumPropertiesToStringify; i++) {
const key = keys[i];
const tmp = stringifySimple(key, value[key], stack);
if (tmp !== undefined) {
res += `${separator}${strEscape(key)}:${tmp}`;
separator = ',';
}
}
if (keyLength > maximumBreadth) {
const removedKeys = keyLength - maximumBreadth;
res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"`;
}
stack.pop();
return `{${res}}`;
}
case 'number':
return isFinite(value) ? String(value) : fail ? fail(value) : 'null';
case 'boolean':
return value ? 'true' : 'false';
case 'undefined':
return undefined;
case 'bigint':
if (bigint) {
return String(value);
}
// fallthrough
default:
return fail ? fail(value) : undefined;
}
}
function stringify(value, replacer, space) {
if (arguments.length > 1) {
let spacer = '';
if (typeof space === 'number') {
spacer = ' '.repeat(Math.min(space, 10));
}
else if (typeof space === 'string') {
spacer = space.slice(0, 10);
}
if (replacer != null) {
if (typeof replacer === 'function') {
return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '');
}
if (Array.isArray(replacer)) {
return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '');
}
}
if (spacer.length !== 0) {
return stringifyIndent('', value, [], spacer, '');
}
}
return stringifySimple('', value, []);
}
return stringify;
}