Spaces:
Runtime error
Runtime error
import { | |
DevalueError, | |
get_type, | |
is_plain_object, | |
is_primitive, | |
stringify_string | |
} from './utils.js'; | |
import { | |
HOLE, | |
NAN, | |
NEGATIVE_INFINITY, | |
NEGATIVE_ZERO, | |
POSITIVE_INFINITY, | |
UNDEFINED | |
} from './constants.js'; | |
/** | |
* Turn a value into a JSON string that can be parsed with `devalue.parse` | |
* @param {any} value | |
* @param {Record<string, (value: any) => any>} [reducers] | |
*/ | |
export function stringify(value, reducers) { | |
/** @type {any[]} */ | |
const stringified = []; | |
/** @type {Map<any, number>} */ | |
const indexes = new Map(); | |
/** @type {Array<{ key: string, fn: (value: any) => any }>} */ | |
const custom = []; | |
for (const key in reducers) { | |
custom.push({ key, fn: reducers[key] }); | |
} | |
/** @type {string[]} */ | |
const keys = []; | |
let p = 0; | |
/** @param {any} thing */ | |
function flatten(thing) { | |
if (typeof thing === 'function') { | |
throw new DevalueError(`Cannot stringify a function`, keys); | |
} | |
if (indexes.has(thing)) return indexes.get(thing); | |
if (thing === undefined) return UNDEFINED; | |
if (Number.isNaN(thing)) return NAN; | |
if (thing === Infinity) return POSITIVE_INFINITY; | |
if (thing === -Infinity) return NEGATIVE_INFINITY; | |
if (thing === 0 && 1 / thing < 0) return NEGATIVE_ZERO; | |
const index = p++; | |
indexes.set(thing, index); | |
for (const { key, fn } of custom) { | |
const value = fn(thing); | |
if (value) { | |
stringified[index] = `["${key}",${flatten(value)}]`; | |
return index; | |
} | |
} | |
let str = ''; | |
if (is_primitive(thing)) { | |
str = stringify_primitive(thing); | |
} else { | |
const type = get_type(thing); | |
switch (type) { | |
case 'Number': | |
case 'String': | |
case 'Boolean': | |
str = `["Object",${stringify_primitive(thing)}]`; | |
break; | |
case 'BigInt': | |
str = `["BigInt",${thing}]`; | |
break; | |
case 'Date': | |
str = `["Date","${thing.toISOString()}"]`; | |
break; | |
case 'RegExp': | |
const { source, flags } = thing; | |
str = flags | |
? `["RegExp",${stringify_string(source)},"${flags}"]` | |
: `["RegExp",${stringify_string(source)}]`; | |
break; | |
case 'Array': | |
str = '['; | |
for (let i = 0; i < thing.length; i += 1) { | |
if (i > 0) str += ','; | |
if (i in thing) { | |
keys.push(`[${i}]`); | |
str += flatten(thing[i]); | |
keys.pop(); | |
} else { | |
str += HOLE; | |
} | |
} | |
str += ']'; | |
break; | |
case 'Set': | |
str = '["Set"'; | |
for (const value of thing) { | |
str += `,${flatten(value)}`; | |
} | |
str += ']'; | |
break; | |
case 'Map': | |
str = '["Map"'; | |
for (const [key, value] of thing) { | |
keys.push( | |
`.get(${is_primitive(key) ? stringify_primitive(key) : '...'})` | |
); | |
str += `,${flatten(key)},${flatten(value)}`; | |
} | |
str += ']'; | |
break; | |
default: | |
if (!is_plain_object(thing)) { | |
throw new DevalueError( | |
`Cannot stringify arbitrary non-POJOs`, | |
keys | |
); | |
} | |
if (Object.getOwnPropertySymbols(thing).length > 0) { | |
throw new DevalueError( | |
`Cannot stringify POJOs with symbolic keys`, | |
keys | |
); | |
} | |
if (Object.getPrototypeOf(thing) === null) { | |
str = '["null"'; | |
for (const key in thing) { | |
keys.push(`.${key}`); | |
str += `,${stringify_string(key)},${flatten(thing[key])}`; | |
keys.pop(); | |
} | |
str += ']'; | |
} else { | |
str = '{'; | |
let started = false; | |
for (const key in thing) { | |
if (started) str += ','; | |
started = true; | |
keys.push(`.${key}`); | |
str += `${stringify_string(key)}:${flatten(thing[key])}`; | |
keys.pop(); | |
} | |
str += '}'; | |
} | |
} | |
} | |
stringified[index] = str; | |
return index; | |
} | |
const index = flatten(value); | |
// special case — value is represented as a negative index | |
if (index < 0) return `${index}`; | |
return `[${stringified.join(',')}]`; | |
} | |
/** | |
* @param {any} thing | |
* @returns {string} | |
*/ | |
function stringify_primitive(thing) { | |
const type = typeof thing; | |
if (type === 'string') return stringify_string(thing); | |
if (thing instanceof String) return stringify_string(thing.toString()); | |
if (thing === void 0) return UNDEFINED.toString(); | |
if (thing === 0 && 1 / thing < 0) return NEGATIVE_ZERO.toString(); | |
if (type === 'bigint') return `["BigInt","${thing}"]`; | |
return String(thing); | |
} | |