625 lines
19 KiB
JavaScript
625 lines
19 KiB
JavaScript
'use strict'
|
|
|
|
const { hasOwnProperty } = Object.prototype
|
|
|
|
const stringify = configure()
|
|
|
|
// @ts-expect-error
|
|
stringify.configure = configure
|
|
// @ts-expect-error
|
|
stringify.stringify = stringify
|
|
|
|
// @ts-expect-error
|
|
stringify.default = stringify
|
|
|
|
// @ts-expect-error used for named export
|
|
exports.stringify = stringify
|
|
// @ts-expect-error used for named export
|
|
exports.configure = configure
|
|
|
|
module.exports = stringify
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.indexOf(value) !== -1) {
|
|
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 ? tmp : 'null'
|
|
res += join
|
|
}
|
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)
|
|
res += tmp !== undefined ? tmp : 'null'
|
|
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 ? '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.indexOf(value) !== -1) {
|
|
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 ? tmp : 'null'
|
|
res += join
|
|
}
|
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)
|
|
res += tmp !== undefined ? tmp : 'null'
|
|
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 ? '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.indexOf(value) !== -1) {
|
|
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 ? tmp : 'null'
|
|
res += join
|
|
}
|
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)
|
|
res += tmp !== undefined ? tmp : 'null'
|
|
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 ? '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.indexOf(value) !== -1) {
|
|
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 ? tmp : 'null'
|
|
res += ','
|
|
}
|
|
const tmp = stringifySimple(String(i), value[i], stack)
|
|
res += tmp !== undefined ? tmp : 'null'
|
|
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 ? '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
|
|
}
|