2 * Author: Alex Kocharin <alex@kocharin.ru>
3 * GIT: https://github.com/rlidwka/jju
4 * License: WTFPL, grab your copy here: http://www.wtfpl.net/txt/copying/
7 var Uni = require('./unicode')
9 // Fix Function#name on browsers that do not support it (IE)
10 // http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie
11 if (!(function f(){}).name) {
12 Object.defineProperty((function(){}).constructor.prototype, 'name', {
14 var name = this.toString().match(/^\s*function\s*(\S*)\s*\(/)[1]
15 // For better performance only parse once, and then cache the
16 // result through a new accessor for repeated access.
17 Object.defineProperty(this, 'name', { value: name })
24 0: '\\0', // this is not an octal literal
35 var hasOwnProperty = Object.prototype.hasOwnProperty
37 // some people escape those, so I'd copy this to be safe
38 var escapable = /[\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/
40 function _stringify(object, options, recursiveLvl, currentKey) {
41 var json5 = (options.mode === 'json5' || !options.mode)
43 * Opinionated decision warning:
45 * Objects are serialized in the following form:
46 * { type: 'Class', data: DATA }
48 * Class is supposed to be a function, and new Class(DATA) is
49 * supposed to be equivalent to the original value
51 /*function custom_type() {
53 type: object.constructor.name,
54 data: object.toString()
58 // if add, it's an internal indentation, so we add 1 level and a eol
59 // if !add, it's an ending indentation, so we just indent
60 function indent(str, add) {
61 var prefix = options._prefix ? options._prefix : ''
62 if (!options.indent) return prefix + str
64 var count = recursiveLvl + (add || 0)
65 for (var i=0; i<count; i++) result += options.indent
66 return prefix + result + str + (add ? '\n' : '')
69 function _stringify_key(key) {
70 if (options.quote_keys) return _stringify_str(key)
71 if (String(Number(key)) == key && key[0] != '-') return key
72 if (key == '') return _stringify_str(key)
75 for (var i=0; i<key.length; i++) {
77 if (!Uni.isIdentifierPart(key[i]))
78 return _stringify_str(key)
81 if (!Uni.isIdentifierStart(key[i]))
82 return _stringify_str(key)
85 var chr = key.charCodeAt(i)
92 result += '\\u' + ('0000' + chr.toString(16)).slice(-4)
96 if (escapable.exec(key[i])) {
97 result += '\\u' + ('0000' + chr.toString(16)).slice(-4)
108 function _stringify_str(key) {
109 var quote = options.quote
110 var quoteChr = quote.charCodeAt(0)
113 for (var i=0; i<key.length; i++) {
114 var chr = key.charCodeAt(i)
117 if (chr === 0 && json5) {
119 } else if (chr >= 8 && chr <= 13 && (json5 || chr !== 11)) {
120 result += special_chars[chr]
122 result += '\\x0' + chr.toString(16)
124 result += '\\u000' + chr.toString(16)
127 } else if (chr < 0x20) {
129 result += '\\x' + chr.toString(16)
131 result += '\\u00' + chr.toString(16)
134 } else if (chr >= 0x20 && chr < 0x80) {
136 if (chr === 47 && i && key[i-1] === '<') {
137 // escaping slashes in </script>
138 result += '\\' + key[i]
140 } else if (chr === 92) {
143 } else if (chr === quoteChr) {
144 result += '\\' + quote
150 } else if (options.ascii || Uni.isLineTerminator(key[i]) || escapable.exec(key[i])) {
153 result += '\\x' + chr.toString(16)
155 result += '\\u00' + chr.toString(16)
158 } else if (chr < 0x1000) {
159 result += '\\u0' + chr.toString(16)
161 } else if (chr < 0x10000) {
162 result += '\\u' + chr.toString(16)
165 throw Error('weird codepoint')
171 return quote + result + quote
174 function _stringify_object() {
175 if (object === null) return 'null'
180 if (Array.isArray(object)) {
182 for (var i=0; i<object.length; i++) {
183 var s = _stringify(object[i], options, recursiveLvl+1, String(i))
184 if (s === undefined) s = 'null'
191 var fn = function(key) {
192 var t = _stringify(object[key], options, recursiveLvl+1, key)
193 if (t !== undefined) {
194 t = _stringify_key(key) + ':' + (options.indent ? ' ' : '') + t + ','
200 if (Array.isArray(options.replacer)) {
201 for (var i=0; i<options.replacer.length; i++)
202 if (hasOwnProperty.call(object, options.replacer[i]))
203 fn(options.replacer[i])
205 var keys = Object.keys(object)
206 if (options.sort_keys)
207 keys = keys.sort(typeof(options.sort_keys) === 'function'
208 ? options.sort_keys : undefined)
213 // objects shorter than 30 characters are always inlined
214 // objects longer than 60 characters are always splitted to multiple lines
215 // anything in the middle depends on indentation level
217 if (options.indent && (len > options._splitMax - recursiveLvl * options.indent.length || len > options._splitMin) ) {
218 // remove trailing comma in multiline if asked to
219 if (options.no_trailing_comma && result.length) {
220 result[result.length-1] = result[result.length-1].substring(0, result[result.length-1].length-1)
223 var innerStuff = result.map(function(x) {return indent(x, 1)}).join('')
225 + (options.indent ? '\n' : '')
229 // always remove trailing comma in one-lined arrays
231 result[result.length-1] = result[result.length-1].substring(0, result[result.length-1].length-1)
234 var innerStuff = result.join(options.indent ? ' ' : '')
241 function _stringify_nonobject(object) {
242 if (typeof(options.replacer) === 'function') {
243 object = options.replacer.call(null, currentKey, object)
246 switch(typeof(object)) {
248 return _stringify_str(object)
251 if (object === 0 && 1/object < 0) {
252 // Opinionated decision warning:
254 // I want cross-platform negative zero in all js engines
255 // I know they're equal, but why lose that tiny bit of
256 // information needlessly?
259 if (!json5 && !Number.isFinite(object)) {
260 // json don't support infinity (= sucks)
263 return object.toString()
266 return object.toString()
272 // return custom_type()
275 // fallback for something weird
276 return JSON.stringify(object)
280 if (options._stringify_key) {
281 return _stringify_key(object)
284 if (typeof(object) === 'object') {
285 if (object === null) return 'null'
288 if (typeof(str = object.toJSON5) === 'function' && options.mode !== 'json') {
289 object = str.call(object, currentKey)
291 } else if (typeof(str = object.toJSON) === 'function') {
292 object = str.call(object, currentKey)
295 if (object === null) return 'null'
296 if (typeof(object) !== 'object') return _stringify_nonobject(object)
298 if (object.constructor === Number || object.constructor === Boolean || object.constructor === String) {
299 object = object.valueOf()
300 return _stringify_nonobject(object)
302 } else if (object.constructor === Date) {
303 // only until we can't do better
304 return _stringify_nonobject(object.toISOString())
307 if (typeof(options.replacer) === 'function') {
308 object = options.replacer.call(null, currentKey, object)
309 if (typeof(object) !== 'object') return _stringify_nonobject(object)
312 return _stringify_object(object)
315 return _stringify_nonobject(object)
320 * stringify(value, options)
322 * stringify(value, replacer, space)
327 * replacer - function or array
328 * space - boolean or number or string
330 module.exports.stringify = function stringifyJSON(object, options, _space) {
331 // support legacy syntax
332 if (typeof(options) === 'function' || Array.isArray(options)) {
336 } else if (typeof(options) === 'object' && options !== null) {
341 if (_space != null) options.indent = _space
343 if (options.indent == null) options.indent = '\t'
344 if (options.quote == null) options.quote = "'"
345 if (options.ascii == null) options.ascii = false
346 if (options.mode == null) options.mode = 'json5'
348 if (options.mode === 'json' || options.mode === 'cjson') {
349 // json only supports double quotes (= sucks)
352 // json don't support trailing commas (= sucks)
353 options.no_trailing_comma = true
355 // json don't support unquoted property names (= sucks)
356 options.quote_keys = true
359 // why would anyone use such objects?
360 if (typeof(options.indent) === 'object') {
361 if (options.indent.constructor === Number
362 || options.indent.constructor === Boolean
363 || options.indent.constructor === String)
364 options.indent = options.indent.valueOf()
367 // gap is capped at 10 characters
368 if (typeof(options.indent) === 'number') {
369 if (options.indent >= 0) {
370 options.indent = Array(Math.min(~~options.indent, 10) + 1).join(' ')
372 options.indent = false
374 } else if (typeof(options.indent) === 'string') {
375 options.indent = options.indent.substr(0, 10)
378 if (options._splitMin == null) options._splitMin = 50
379 if (options._splitMax == null) options._splitMax = 70
381 return _stringify(object, options, 0, '')