/* * Author: Alex Kocharin * GIT: https://github.com/rlidwka/jju * License: WTFPL, grab your copy here: http://www.wtfpl.net/txt/copying/ */ var Uni = require('./unicode') // Fix Function#name on browsers that do not support it (IE) // http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie if (!(function f(){}).name) { Object.defineProperty((function(){}).constructor.prototype, 'name', { get: function() { var name = this.toString().match(/^\s*function\s*(\S*)\s*\(/)[1] // For better performance only parse once, and then cache the // result through a new accessor for repeated access. Object.defineProperty(this, 'name', { value: name }) return name } }) } var special_chars = { 0: '\\0', // this is not an octal literal 8: '\\b', 9: '\\t', 10: '\\n', 11: '\\v', 12: '\\f', 13: '\\r', 92: '\\\\', } // for oddballs var hasOwnProperty = Object.prototype.hasOwnProperty // some people escape those, so I'd copy this to be safe var escapable = /[\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/ function _stringify(object, options, recursiveLvl, currentKey) { var json5 = (options.mode === 'json5' || !options.mode) /* * Opinionated decision warning: * * Objects are serialized in the following form: * { type: 'Class', data: DATA } * * Class is supposed to be a function, and new Class(DATA) is * supposed to be equivalent to the original value */ /*function custom_type() { return stringify({ type: object.constructor.name, data: object.toString() }) }*/ // if add, it's an internal indentation, so we add 1 level and a eol // if !add, it's an ending indentation, so we just indent function indent(str, add) { var prefix = options._prefix ? options._prefix : '' if (!options.indent) return prefix + str var result = '' var count = recursiveLvl + (add || 0) for (var i=0; i 0) { if (!Uni.isIdentifierPart(key[i])) return _stringify_str(key) } else { if (!Uni.isIdentifierStart(key[i])) return _stringify_str(key) } var chr = key.charCodeAt(i) if (options.ascii) { if (chr < 0x80) { result += key[i] } else { result += '\\u' + ('0000' + chr.toString(16)).slice(-4) } } else { if (escapable.exec(key[i])) { result += '\\u' + ('0000' + chr.toString(16)).slice(-4) } else { result += key[i] } } } return result } function _stringify_str(key) { var quote = options.quote var quoteChr = quote.charCodeAt(0) var result = '' for (var i=0; i= 8 && chr <= 13 && (json5 || chr !== 11)) { result += special_chars[chr] } else if (json5) { result += '\\x0' + chr.toString(16) } else { result += '\\u000' + chr.toString(16) } } else if (chr < 0x20) { if (json5) { result += '\\x' + chr.toString(16) } else { result += '\\u00' + chr.toString(16) } } else if (chr >= 0x20 && chr < 0x80) { // ascii range if (chr === 47 && i && key[i-1] === '<') { // escaping slashes in result += '\\' + key[i] } else if (chr === 92) { result += '\\\\' } else if (chr === quoteChr) { result += '\\' + quote } else { result += key[i] } } else if (options.ascii || Uni.isLineTerminator(key[i]) || escapable.exec(key[i])) { if (chr < 0x100) { if (json5) { result += '\\x' + chr.toString(16) } else { result += '\\u00' + chr.toString(16) } } else if (chr < 0x1000) { result += '\\u0' + chr.toString(16) } else if (chr < 0x10000) { result += '\\u' + chr.toString(16) } else { throw Error('weird codepoint') } } else { result += key[i] } } return quote + result + quote } function _stringify_object() { if (object === null) return 'null' var result = [] , len = 0 , braces if (Array.isArray(object)) { braces = '[]' for (var i=0; i options._splitMax - recursiveLvl * options.indent.length || len > options._splitMin) ) { // remove trailing comma in multiline if asked to if (options.no_trailing_comma && result.length) { result[result.length-1] = result[result.length-1].substring(0, result[result.length-1].length-1) } var innerStuff = result.map(function(x) {return indent(x, 1)}).join('') return braces[0] + (options.indent ? '\n' : '') + innerStuff + indent(braces[1]) } else { // always remove trailing comma in one-lined arrays if (result.length) { result[result.length-1] = result[result.length-1].substring(0, result[result.length-1].length-1) } var innerStuff = result.join(options.indent ? ' ' : '') return braces[0] + innerStuff + braces[1] } } function _stringify_nonobject(object) { if (typeof(options.replacer) === 'function') { object = options.replacer.call(null, currentKey, object) } switch(typeof(object)) { case 'string': return _stringify_str(object) case 'number': if (object === 0 && 1/object < 0) { // Opinionated decision warning: // // I want cross-platform negative zero in all js engines // I know they're equal, but why lose that tiny bit of // information needlessly? return '-0' } if (!json5 && !Number.isFinite(object)) { // json don't support infinity (= sucks) return 'null' } return object.toString() case 'boolean': return object.toString() case 'undefined': return undefined case 'function': // return custom_type() default: // fallback for something weird return JSON.stringify(object) } } if (options._stringify_key) { return _stringify_key(object) } if (typeof(object) === 'object') { if (object === null) return 'null' var str if (typeof(str = object.toJSON5) === 'function' && options.mode !== 'json') { object = str.call(object, currentKey) } else if (typeof(str = object.toJSON) === 'function') { object = str.call(object, currentKey) } if (object === null) return 'null' if (typeof(object) !== 'object') return _stringify_nonobject(object) if (object.constructor === Number || object.constructor === Boolean || object.constructor === String) { object = object.valueOf() return _stringify_nonobject(object) } else if (object.constructor === Date) { // only until we can't do better return _stringify_nonobject(object.toISOString()) } else { if (typeof(options.replacer) === 'function') { object = options.replacer.call(null, currentKey, object) if (typeof(object) !== 'object') return _stringify_nonobject(object) } return _stringify_object(object) } } else { return _stringify_nonobject(object) } } /* * stringify(value, options) * or * stringify(value, replacer, space) * * where: * value - anything * options - object * replacer - function or array * space - boolean or number or string */ module.exports.stringify = function stringifyJSON(object, options, _space) { // support legacy syntax if (typeof(options) === 'function' || Array.isArray(options)) { options = { replacer: options } } else if (typeof(options) === 'object' && options !== null) { // nothing to do } else { options = {} } if (_space != null) options.indent = _space if (options.indent == null) options.indent = '\t' if (options.quote == null) options.quote = "'" if (options.ascii == null) options.ascii = false if (options.mode == null) options.mode = 'json5' if (options.mode === 'json' || options.mode === 'cjson') { // json only supports double quotes (= sucks) options.quote = '"' // json don't support trailing commas (= sucks) options.no_trailing_comma = true // json don't support unquoted property names (= sucks) options.quote_keys = true } // why would anyone use such objects? if (typeof(options.indent) === 'object') { if (options.indent.constructor === Number || options.indent.constructor === Boolean || options.indent.constructor === String) options.indent = options.indent.valueOf() } // gap is capped at 10 characters if (typeof(options.indent) === 'number') { if (options.indent >= 0) { options.indent = Array(Math.min(~~options.indent, 10) + 1).join(' ') } else { options.indent = false } } else if (typeof(options.indent) === 'string') { options.indent = options.indent.substr(0, 10) } if (options._splitMin == null) options._splitMin = 50 if (options._splitMax == null) options._splitMax = 70 return _stringify(object, options, 0, '') }