|
import { |
|
DevalueError, |
|
enumerable_symbols, |
|
escaped, |
|
get_type, |
|
is_plain_object, |
|
is_primitive, |
|
stringify_string |
|
} from './utils.js'; |
|
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; |
|
const unsafe_chars = /[<\b\f\n\r\t\0\u2028\u2029]/g; |
|
const reserved = |
|
/^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function uneval(value, replacer) { |
|
const counts = new Map(); |
|
|
|
|
|
const keys = []; |
|
|
|
const custom = new Map(); |
|
|
|
|
|
function walk(thing) { |
|
if (typeof thing === 'function') { |
|
throw new DevalueError(`Cannot stringify a function`, keys); |
|
} |
|
|
|
if (!is_primitive(thing)) { |
|
if (counts.has(thing)) { |
|
counts.set(thing, counts.get(thing) + 1); |
|
return; |
|
} |
|
|
|
counts.set(thing, 1); |
|
|
|
if (replacer) { |
|
const str = replacer(thing); |
|
|
|
if (typeof str === 'string') { |
|
custom.set(thing, str); |
|
return; |
|
} |
|
} |
|
|
|
const type = get_type(thing); |
|
|
|
switch (type) { |
|
case 'Number': |
|
case 'BigInt': |
|
case 'String': |
|
case 'Boolean': |
|
case 'Date': |
|
case 'RegExp': |
|
return; |
|
|
|
case 'Array': |
|
(thing).forEach((value, i) => { |
|
keys.push(`[${i}]`); |
|
walk(value); |
|
keys.pop(); |
|
}); |
|
break; |
|
|
|
case 'Set': |
|
Array.from(thing).forEach(walk); |
|
break; |
|
|
|
case 'Map': |
|
for (const [key, value] of thing) { |
|
keys.push( |
|
`.get(${is_primitive(key) ? stringify_primitive(key) : '...'})` |
|
); |
|
walk(value); |
|
keys.pop(); |
|
} |
|
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 |
|
); |
|
} |
|
|
|
for (const key in thing) { |
|
keys.push(`.${key}`); |
|
walk(thing[key]); |
|
keys.pop(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
walk(value); |
|
|
|
const names = new Map(); |
|
|
|
Array.from(counts) |
|
.filter((entry) => entry[1] > 1) |
|
.sort((a, b) => b[1] - a[1]) |
|
.forEach((entry, i) => { |
|
names.set(entry[0], get_name(i)); |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
function stringify(thing) { |
|
if (names.has(thing)) { |
|
return names.get(thing); |
|
} |
|
|
|
if (is_primitive(thing)) { |
|
return stringify_primitive(thing); |
|
} |
|
|
|
if (custom.has(thing)) { |
|
return custom.get(thing); |
|
} |
|
|
|
const type = get_type(thing); |
|
|
|
switch (type) { |
|
case 'Number': |
|
case 'String': |
|
case 'Boolean': |
|
return `Object(${stringify(thing.valueOf())})`; |
|
|
|
case 'RegExp': |
|
return `new RegExp(${stringify_string(thing.source)}, "${ |
|
thing.flags |
|
}")`; |
|
|
|
case 'Date': |
|
return `new Date(${thing.getTime()})`; |
|
|
|
case 'Array': |
|
const members = (thing).map((v, i) => |
|
i in thing ? stringify(v) : '' |
|
); |
|
const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ','; |
|
return `[${members.join(',')}${tail}]`; |
|
|
|
case 'Set': |
|
case 'Map': |
|
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`; |
|
|
|
default: |
|
const obj = `{${Object.keys(thing) |
|
.map((key) => `${safe_key(key)}:${stringify(thing[key])}`) |
|
.join(',')}}`; |
|
const proto = Object.getPrototypeOf(thing); |
|
if (proto === null) { |
|
return Object.keys(thing).length > 0 |
|
? `Object.assign(Object.create(null),${obj})` |
|
: `Object.create(null)`; |
|
} |
|
|
|
return obj; |
|
} |
|
} |
|
|
|
const str = stringify(value); |
|
|
|
if (names.size) { |
|
|
|
const params = []; |
|
|
|
|
|
const statements = []; |
|
|
|
|
|
const values = []; |
|
|
|
names.forEach((name, thing) => { |
|
params.push(name); |
|
|
|
if (custom.has(thing)) { |
|
values.push( (custom.get(thing))); |
|
return; |
|
} |
|
|
|
if (is_primitive(thing)) { |
|
values.push(stringify_primitive(thing)); |
|
return; |
|
} |
|
|
|
const type = get_type(thing); |
|
|
|
switch (type) { |
|
case 'Number': |
|
case 'String': |
|
case 'Boolean': |
|
values.push(`Object(${stringify(thing.valueOf())})`); |
|
break; |
|
|
|
case 'RegExp': |
|
values.push(thing.toString()); |
|
break; |
|
|
|
case 'Date': |
|
values.push(`new Date(${thing.getTime()})`); |
|
break; |
|
|
|
case 'Array': |
|
values.push(`Array(${thing.length})`); |
|
(thing).forEach((v, i) => { |
|
statements.push(`${name}[${i}]=${stringify(v)}`); |
|
}); |
|
break; |
|
|
|
case 'Set': |
|
values.push(`new Set`); |
|
statements.push( |
|
`${name}.${Array.from(thing) |
|
.map((v) => `add(${stringify(v)})`) |
|
.join('.')}` |
|
); |
|
break; |
|
|
|
case 'Map': |
|
values.push(`new Map`); |
|
statements.push( |
|
`${name}.${Array.from(thing) |
|
.map(([k, v]) => `set(${stringify(k)}, ${stringify(v)})`) |
|
.join('.')}` |
|
); |
|
break; |
|
|
|
default: |
|
values.push( |
|
Object.getPrototypeOf(thing) === null ? 'Object.create(null)' : '{}' |
|
); |
|
Object.keys(thing).forEach((key) => { |
|
statements.push( |
|
`${name}${safe_prop(key)}=${stringify(thing[key])}` |
|
); |
|
}); |
|
} |
|
}); |
|
|
|
statements.push(`return ${str}`); |
|
|
|
return `(function(${params.join(',')}){${statements.join( |
|
';' |
|
)}}(${values.join(',')}))`; |
|
} else { |
|
return str; |
|
} |
|
} |
|
|
|
|
|
function get_name(num) { |
|
let name = ''; |
|
|
|
do { |
|
name = chars[num % chars.length] + name; |
|
num = ~~(num / chars.length) - 1; |
|
} while (num >= 0); |
|
|
|
return reserved.test(name) ? `${name}0` : name; |
|
} |
|
|
|
|
|
function escape_unsafe_char(c) { |
|
return escaped[c] || c; |
|
} |
|
|
|
|
|
function escape_unsafe_chars(str) { |
|
return str.replace(unsafe_chars, escape_unsafe_char); |
|
} |
|
|
|
|
|
function safe_key(key) { |
|
return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) |
|
? key |
|
: escape_unsafe_chars(JSON.stringify(key)); |
|
} |
|
|
|
|
|
function safe_prop(key) { |
|
return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) |
|
? `.${key}` |
|
: `[${escape_unsafe_chars(JSON.stringify(key))}]`; |
|
} |
|
|
|
|
|
function stringify_primitive(thing) { |
|
if (typeof thing === 'string') return stringify_string(thing); |
|
if (thing === void 0) return 'void 0'; |
|
if (thing === 0 && 1 / thing < 0) return '-0'; |
|
const str = String(thing); |
|
if (typeof thing === 'number') return str.replace(/^(-)?0\./, '$1.'); |
|
if (typeof thing === 'bigint') return thing + 'n'; |
|
return str; |
|
} |
|
|