|
18 | 18 | * source to match single quotes instead of double.
|
19 | 19 | *
|
20 | 20 | * Source: https://github.com/douglascrockford/JSON-js/blob/master/json2.js
|
21 |
| - * |
22 |
| - * @type {RegExp} |
23 | 21 | */
|
24 | 22 | var ESCAPABLE = /[\\\'\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
25 | 23 |
|
26 | 24 | /**
|
27 | 25 | * Map of characters to escape characters.
|
28 |
| - * |
29 |
| - * @type {Object} |
30 | 26 | */
|
31 | 27 | var META_CHARS = {
|
32 | 28 | '\b': '\\b',
|
|
42 | 38 | /**
|
43 | 39 | * Escape any character into its literal JavaScript string.
|
44 | 40 | *
|
45 |
| - * @param {String} char |
46 |
| - * @return {String} |
| 41 | + * @param {string} char |
| 42 | + * @return {string} |
47 | 43 | */
|
48 |
| - var escapeChar = function (char) { |
| 44 | + function escapeChar (char) { |
49 | 45 | var meta = META_CHARS[char];
|
50 | 46 |
|
51 | 47 | return meta || '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4);
|
|
70 | 66 | RESERVED_WORDS[key] = true;
|
71 | 67 | });
|
72 | 68 |
|
| 69 | + /** |
| 70 | + * Test for valid JavaScript identifier. |
| 71 | + */ |
| 72 | + var IS_VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; |
| 73 | + |
73 | 74 | /**
|
74 | 75 | * Check if a variable name is valid.
|
75 | 76 | *
|
76 |
| - * @param {String} name |
77 |
| - * @return {Boolean} |
| 77 | + * @param {string} name |
| 78 | + * @return {boolean} |
78 | 79 | */
|
79 |
| - var isValidVariableName = function (name) { |
80 |
| - return !RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); |
81 |
| - }; |
| 80 | + function isValidVariableName (name) { |
| 81 | + return !RESERVED_WORDS[name] && IS_VALID_IDENTIFIER.test(name); |
| 82 | + } |
82 | 83 |
|
83 | 84 | /**
|
84 | 85 | * Return the global variable name.
|
85 | 86 | *
|
86 |
| - * @return {String} |
| 87 | + * @return {string} |
87 | 88 | */
|
88 |
| - var toGlobalVariable = function (value, indent, stringify) { |
| 89 | + function toGlobalVariable (value) { |
89 | 90 | return 'Function(' + stringify('return this;') + ')()';
|
90 |
| - }; |
| 91 | + } |
91 | 92 |
|
92 | 93 | /**
|
93 |
| - * Convert JavaScript objects into strings. |
| 94 | + * Serialize the path to a string. |
94 | 95 | *
|
95 |
| - * @type {Object} |
| 96 | + * @param {Array} path |
| 97 | + * @return {string} |
| 98 | + */ |
| 99 | + function toPath (path) { |
| 100 | + var result = ''; |
| 101 | + |
| 102 | + for (var i = 0; i < path.length; i++) { |
| 103 | + if (isValidVariableName(path[i])) { |
| 104 | + result += '.' + path[i]; |
| 105 | + } else { |
| 106 | + result += '[' + stringify(path[i]) + ']'; |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + return result; |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Convert JavaScript objects into strings. |
96 | 115 | */
|
97 | 116 | var OBJECT_TYPES = {
|
98 |
| - '[object Array]': function (array, indent, stringify) { |
| 117 | + '[object Array]': function (array, indent, next) { |
99 | 118 | // Map array values to their stringified values with correct indentation.
|
100 |
| - var values = array.map(function (value) { |
101 |
| - var str = stringify(value); |
| 119 | + var values = array.map(function (value, index) { |
| 120 | + var str = next(value, index); |
102 | 121 |
|
103 | 122 | if (str === undefined) {
|
104 | 123 | return String(str)
|
|
114 | 133 |
|
115 | 134 | return '[' + values + ']';
|
116 | 135 | },
|
117 |
| - '[object Object]': function (object, indent, stringify) { |
| 136 | + '[object Object]': function (object, indent, next) { |
118 | 137 | if (typeof Buffer === 'function' && Buffer.isBuffer(object)) {
|
119 | 138 | return 'new Buffer(' + stringify(object.toString()) + ')';
|
120 | 139 | }
|
121 | 140 |
|
122 | 141 | // Iterate over object keys and concat string together.
|
123 | 142 | var values = Object.keys(object).reduce(function (values, key) {
|
124 |
| - var value = stringify(object[key]); |
| 143 | + var value = next(object[key], key); |
125 | 144 |
|
126 | 145 | // Omit `undefined` object values.
|
127 | 146 | if (value === undefined) {
|
|
182 | 201 |
|
183 | 202 | /**
|
184 | 203 | * Convert JavaScript primitives into strings.
|
185 |
| - * |
186 |
| - * @type {Object} |
187 | 204 | */
|
188 | 205 | var PRIMITIVE_TYPES = {
|
189 | 206 | 'string': function (string) {
|
|
200 | 217 | * Convert any value to a string.
|
201 | 218 | *
|
202 | 219 | * @param {*} value
|
203 |
| - * @param {String} indent |
204 |
| - * @param {Function} stringify |
205 |
| - * @return {String} |
| 220 | + * @param {string} indent |
| 221 | + * @param {Function} next |
| 222 | + * @return {string} |
206 | 223 | */
|
207 |
| - var stringify = function (value, indent, stringify) { |
| 224 | + function stringify (value, indent, next) { |
208 | 225 | // Convert primitives into strings.
|
209 | 226 | if (Object(value) !== value) {
|
210 |
| - return PRIMITIVE_TYPES[typeof value](value, indent, stringify); |
| 227 | + return PRIMITIVE_TYPES[typeof value](value, indent, next); |
211 | 228 | }
|
212 | 229 |
|
213 | 230 | // Use the internal object string to select stringification method.
|
214 | 231 | var toString = OBJECT_TYPES[Object.prototype.toString.call(value)];
|
215 | 232 |
|
216 | 233 | // Convert objects into strings.
|
217 |
| - return toString && toString(value, indent, stringify); |
218 |
| - }; |
| 234 | + return toString ? toString(value, indent, next) : undefined; |
| 235 | + } |
219 | 236 |
|
220 | 237 | /**
|
221 | 238 | * Stringify an object into the literal string.
|
222 | 239 | *
|
223 |
| - * @param {Object} value |
| 240 | + * @param {*} value |
224 | 241 | * @param {Function} [replacer]
|
225 |
| - * @param {(Number|String)} [space] |
226 |
| - * @param {Object} [options] |
227 |
| - * @return {String} |
| 242 | + * @param {(number|string)} [space] |
| 243 | + * @param {Object} [options] |
| 244 | + * @return {string} |
228 | 245 | */
|
229 | 246 | return function (value, replacer, space, options) {
|
230 | 247 | options = options || {}
|
|
234 | 251 | space = new Array(Math.max(0, space|0) + 1).join(' ');
|
235 | 252 | }
|
236 | 253 |
|
237 |
| - var maxDepth = options.maxDepth || 200; |
| 254 | + var maxDepth = Number(options.maxDepth) || 100; |
| 255 | + var references = !!options.references; |
238 | 256 |
|
239 |
| - var depth = 0; |
240 |
| - var cache = []; |
| 257 | + var path = []; |
| 258 | + var stack = []; |
| 259 | + var encountered = []; |
| 260 | + var paths = []; |
| 261 | + var restore = []; |
241 | 262 |
|
242 | 263 | /**
|
243 |
| - * Handle recursion by checking if we've visited this node every iteration. |
| 264 | + * Stringify the next value in the stack. |
244 | 265 | *
|
245 | 266 | * @param {*} value
|
246 |
| - * @return {String} |
| 267 | + * @param {string} key |
| 268 | + * @return {string} |
247 | 269 | */
|
248 |
| - var recurse = function (value, next) { |
249 |
| - // If we've already visited this node before, break the recursion. |
250 |
| - if (cache.indexOf(value) > -1 || depth > maxDepth) { |
251 |
| - return; |
252 |
| - } |
| 270 | + function next (value, key) { |
| 271 | + path.push(key); |
| 272 | + var result = recurse(value, stringify); |
| 273 | + path.pop(); |
| 274 | + return result; |
| 275 | + } |
| 276 | + |
| 277 | + /** |
| 278 | + * Handle recursion by checking if we've visited this node every iteration. |
| 279 | + * |
| 280 | + * @param {*} value |
| 281 | + * @param {Function} stringify |
| 282 | + * @return {string} |
| 283 | + */ |
| 284 | + var recurse = references ? |
| 285 | + function (value, stringify) { |
| 286 | + var exists = encountered.indexOf(value); |
| 287 | + |
| 288 | + // Track nodes to restore later. |
| 289 | + if (exists > -1) { |
| 290 | + restore.push(path.slice(), paths[exists]); |
| 291 | + return; |
| 292 | + } |
253 | 293 |
|
254 |
| - // Push the value into the values cache to avoid an infinite loop. |
255 |
| - depth++; |
256 |
| - cache.push(value); |
| 294 | + // Stop when we hit the max depth. |
| 295 | + if (path.length > maxDepth) { |
| 296 | + return; |
| 297 | + } |
| 298 | + |
| 299 | + // Track encountered nodes. |
| 300 | + encountered.push(value); |
| 301 | + paths.push(path.slice()); |
257 | 302 |
|
258 |
| - // Stringify the value and fallback to |
259 |
| - return next(value, space, function (value) { |
260 |
| - var result = recurse(value, next); |
| 303 | + // Stringify the value and fallback to |
| 304 | + return stringify(value, space, next); |
| 305 | + } : |
| 306 | + function (value, stringify) { |
| 307 | + var seen = stack.indexOf(value); |
261 | 308 |
|
262 |
| - depth--; |
263 |
| - cache.pop(); |
| 309 | + if (seen > -1 || path.length > maxDepth) { |
| 310 | + return; |
| 311 | + } |
264 | 312 |
|
265 |
| - return result; |
266 |
| - }); |
267 |
| - }; |
| 313 | + stack.push(value); |
| 314 | + var value = stringify(value, space, next); |
| 315 | + stack.pop(); |
| 316 | + return value; |
| 317 | + }; |
268 | 318 |
|
269 | 319 | // If the user defined a replacer function, make the recursion function
|
270 |
| - // a double step process - `replacer -> stringify -> replacer -> etc`. |
| 320 | + // a double step process - `recurse -> replacer -> stringify`. |
271 | 321 | if (typeof replacer === 'function') {
|
272 |
| - return recurse(value, function (value, space, next) { |
273 |
| - return replacer(value, space, function (value) { |
274 |
| - return stringify(value, space, next); |
| 322 | + var before = recurse |
| 323 | + |
| 324 | + // Intertwine the replacer function with the regular recursion. |
| 325 | + recurse = function (value, stringify) { |
| 326 | + return before(value, function (value, space, next) { |
| 327 | + return replacer(value, space, function (value) { |
| 328 | + return stringify(value, space, next); |
| 329 | + }); |
275 | 330 | });
|
276 |
| - }); |
| 331 | + }; |
| 332 | + } |
| 333 | + |
| 334 | + var result = recurse(value, stringify); |
| 335 | + |
| 336 | + // Attempt to restore circular references. |
| 337 | + if (restore.length) { |
| 338 | + var sep = space ? '\n' : ''; |
| 339 | + var assignment = space ? ' = ' : '='; |
| 340 | + var eol = ';' + sep; |
| 341 | + var before = space ? '(function () {' : '(function(){' |
| 342 | + var after = '}())' |
| 343 | + var results = ['var x' + assignment + result]; |
| 344 | + |
| 345 | + for (var i = 0; i < restore.length; i += 2) { |
| 346 | + results.push('x' + toPath(restore[i]) + assignment + 'x' + toPath(restore[i + 1])); |
| 347 | + } |
| 348 | + |
| 349 | + results.push('return x'); |
| 350 | + |
| 351 | + return before + sep + results.join(eol) + eol + after |
277 | 352 | }
|
278 | 353 |
|
279 |
| - return recurse(value, stringify); |
| 354 | + return result; |
280 | 355 | };
|
281 | 356 | });
|
0 commit comments