199 lines
4.2 KiB
JavaScript
199 lines
4.2 KiB
JavaScript
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);
|
||
}
|