json-literal
Version:
superset of `JSON` adding date, regex, null and octal literals
144 lines (137 loc) • 5.28 kB
JavaScript
var type = require('type-of')
var util = require('util')
var esprima = require('esprima')
module.exports = {parse: parse, stringify: stringify}
function parse(src, options) {
var circular = true
if (options && options.circular === false) {
circular = false
}
var result
var ast = esprima.parse('(' + src.replace(/^\((.*)\)$/, '$1') + ')');
if (ast.body.length !== 1) {
throw new Error('unexpected extra expression');
}
else if (ast.body[0].type !== 'ExpressionStatement') {
throw new Error('expected ExpressionStatement');
}
var root = ast.body[0].expression;
var circulars = []
var result = (function walk(node, path) {
if (node.type === 'Literal') {
return node.value
} else if (node.type === 'UnaryExpression' && node.operator === '-' && node.argument.type === 'Literal' && typeof node.argument.value === 'number') {
return -node.argument.value
} else if (node.type === 'ArrayExpression') {
return node.elements.map(function (e, i) { return walk(e, circular && path.concat([i])) })
} else if (node.type === 'ObjectExpression') {
var obj = {}
for (var i = 0; i < node.properties.length; i++) {
var prop = node.properties[i]
var key = prop.key.type === 'Literal' ? prop.key.value : (prop.key.type === 'Identifier' ? prop.key.name : null)
if (key === '__proto__') {
throw new Error('Cannot assign property __proto__')
}
var value = prop.value === null ? prop.value : walk(prop.value, circular && path.concat([key]))
if (key === null) throw new Error('Object key of type ' + prop.key.type + ' not allowed, expected Literal or Identifier')
obj[key] = value
}
return obj;
} else if (node.type === 'Identifier' && node.name === 'undefined') {
return undefined
} else if (node.type === 'NewExpression' && node.callee.type === 'Identifier' && node.callee.name === 'Date') {
var args = node.arguments.map(walk)
return eval('new Date(' + args.map(JSON.stringify).join(',') + ')')
} else if (node.type === 'NewExpression' && node.callee.type === 'Identifier' && node.callee.name === 'Circular') {
if (!path) {
throw new Error('Circular references are not supported in this location.')
}
circulars.push({from: node.arguments.map(walk), to: path})
return new Circular()
} else {
var ex = new Error('unexpected ' + node.type + ' node')
throw ex
}
}(root, circular && []))
for (var i = 0; i < circulars.length; i++) {
var from = result
for (var x = 0; x < circulars[i].from.length; x++) {
if (circulars[i].from[x] === '__proto__') {
throw new Error('Cannot make circular references to `__proto__`')
}
from = from[circulars[i].from[x]]
}
var to = result
for (var x = 0; x < circulars[i].to.length - 1; x++) {
if (circulars[i].to[x] === '__proto__') {
throw new Error('Cannot make circular references to `__proto__`')
}
to = to[circulars[i].to[x]]
}
if (circulars[i].to[circulars[i].to.length - 1] === '__proto__') {
throw new Error('Cannot make circular references to `__proto__`')
}
to[circulars[i].to[circulars[i].to.length - 1]] = from
}
return result
}
function Circular() {}
function stringify(obj, options) {
var circular = true
if (options && options.circular === false) {
circular = false
}
var sentinel = {}
var circularInputs = []
var circularResults = []
var res = (function walk(node, path) {
switch (type(node)) {
case 'null':
return 'null'
case 'undefined':
return 'undefined'
case 'string':
return JSON.stringify(node)
case 'boolean':
return node.toString()
case 'number':
return node.toString()
}
if (circular) {
if (circularInputs.indexOf(node) != -1) {
return 'new Circular(' + circularResults[circularInputs.indexOf(node)].map(JSON.stringify).join(',') + ')'
}
circularInputs.push(node)
circularResults.push(path)
}
switch (type(node)) {
case 'date':
return 'new Date(' + walk(node.toISOString()) + ')'
case 'regexp':
return util.inspect(node)
case 'arguments':
node = Array.prototype.slice(node)
case 'array':
return '[' + node.map(function (node, i) { return walk(node, circular && path.concat([i]) ) }).map(function (v) { return v === sentinel ? 'undefined' : v }).join(',') + ']'
case 'object':
var partial = []
for (var k in node) {
if (Object.prototype.hasOwnProperty.call(node, k)) {
var v = walk(node[k], circular && path.concat([k]));
if (v !== sentinel) {
partial.push((/^[a-zA-Z]+$/.test(k) ? k : walk(k)) + ':' + v)
}
}
}
return '{' + partial.join(',') + '}'
default:
return sentinel
}
}(obj, circular && []))
if (res === sentinel) {
throw new Error('Cannot stringify ' + type(obj))
} else {
return '(' + res + ')'
}
}