|
import { |
|
DevalueError, |
|
enumerable_symbols, |
|
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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function stringify(value, reducers) { |
|
|
|
const stringified = []; |
|
|
|
|
|
const indexes = new Map(); |
|
|
|
|
|
const custom = []; |
|
for (const key in reducers) { |
|
custom.push({ key, fn: reducers[key] }); |
|
} |
|
|
|
|
|
const keys = []; |
|
|
|
let p = 0; |
|
|
|
|
|
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': |
|
const valid = !isNaN(thing.getDate()); |
|
str = `["Date","${valid ? 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)}`; |
|
keys.pop(); |
|
} |
|
|
|
str += ']'; |
|
break; |
|
|
|
default: |
|
if (!is_plain_object(thing)) { |
|
throw new DevalueError( |
|
`Cannot stringify arbitrary non-POJOs`, |
|
keys |
|
); |
|
} |
|
|
|
if (enumerable_symbols(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); |
|
|
|
|
|
if (index < 0) return `${index}`; |
|
|
|
return `[${stringified.join(',')}]`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|