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 any>} [reducers] */ export function stringify(value, reducers) { /** @type {any[]} */ const stringified = []; /** @type {Map} */ 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); }