Skip to content

Commit 964c5f2

Browse files
authored
Enable restore of repeat (or circular) references (#6)
1 parent 28ea3ed commit 964c5f2

File tree

3 files changed

+186
-70
lines changed

3 files changed

+186
-70
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Build status][travis-image]][travis-url]
66
[![Test coverage][coveralls-image]][coveralls-url]
77

8-
Stringify is to `eval` as `JSON.stringify` is to `JSON.parse`.
8+
> Stringify is to `eval` as `JSON.stringify` is to `JSON.parse`.
99
1010
## Installation
1111

@@ -40,11 +40,12 @@ define(function (require, exports, module) {
4040
javascriptStringify(value[, replacer [, space [, options]]])
4141
```
4242

43-
The API is similar to `JSON.stringify`. However, any value returned by the replacer will be used literally. For this reason, the replacer is passed three arguments - `value`, `indentation` and `stringify`. If you need to continue the stringification process inside your replacer, you can call `stringify` with the updated value.
43+
The API is similar to `JSON.stringify`. However, any value returned by the replacer will be used literally. For this reason, the replacer is passed three arguments - `value`, `indentation` and `stringify`. If you need to continue the stringification process inside your replacer, you can call `stringify(value)` with the new value.
4444

4545
The `options` object allows some additional configuration:
4646

47-
* **maxDepth** The maximum depth to stringify to
47+
* **maxDepth** _(number)_ The maximum depth of values to stringify
48+
* **references** _(boolean)_ Restore circular/repeated references in the object (uses IIFE)
4849

4950
### Examples
5051

javascript-stringify.js

Lines changed: 137 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@
1818
* source to match single quotes instead of double.
1919
*
2020
* Source: https://github.com/douglascrockford/JSON-js/blob/master/json2.js
21-
*
22-
* @type {RegExp}
2321
*/
2422
var ESCAPABLE = /[\\\'\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
2523

2624
/**
2725
* Map of characters to escape characters.
28-
*
29-
* @type {Object}
3026
*/
3127
var META_CHARS = {
3228
'\b': '\\b',
@@ -42,10 +38,10 @@
4238
/**
4339
* Escape any character into its literal JavaScript string.
4440
*
45-
* @param {String} char
46-
* @return {String}
41+
* @param {string} char
42+
* @return {string}
4743
*/
48-
var escapeChar = function (char) {
44+
function escapeChar (char) {
4945
var meta = META_CHARS[char];
5046

5147
return meta || '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4);
@@ -70,35 +66,58 @@
7066
RESERVED_WORDS[key] = true;
7167
});
7268

69+
/**
70+
* Test for valid JavaScript identifier.
71+
*/
72+
var IS_VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
73+
7374
/**
7475
* Check if a variable name is valid.
7576
*
76-
* @param {String} name
77-
* @return {Boolean}
77+
* @param {string} name
78+
* @return {boolean}
7879
*/
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+
}
8283

8384
/**
8485
* Return the global variable name.
8586
*
86-
* @return {String}
87+
* @return {string}
8788
*/
88-
var toGlobalVariable = function (value, indent, stringify) {
89+
function toGlobalVariable (value) {
8990
return 'Function(' + stringify('return this;') + ')()';
90-
};
91+
}
9192

9293
/**
93-
* Convert JavaScript objects into strings.
94+
* Serialize the path to a string.
9495
*
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.
96115
*/
97116
var OBJECT_TYPES = {
98-
'[object Array]': function (array, indent, stringify) {
117+
'[object Array]': function (array, indent, next) {
99118
// 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);
102121

103122
if (str === undefined) {
104123
return String(str)
@@ -114,14 +133,14 @@
114133

115134
return '[' + values + ']';
116135
},
117-
'[object Object]': function (object, indent, stringify) {
136+
'[object Object]': function (object, indent, next) {
118137
if (typeof Buffer === 'function' && Buffer.isBuffer(object)) {
119138
return 'new Buffer(' + stringify(object.toString()) + ')';
120139
}
121140

122141
// Iterate over object keys and concat string together.
123142
var values = Object.keys(object).reduce(function (values, key) {
124-
var value = stringify(object[key]);
143+
var value = next(object[key], key);
125144

126145
// Omit `undefined` object values.
127146
if (value === undefined) {
@@ -182,8 +201,6 @@
182201

183202
/**
184203
* Convert JavaScript primitives into strings.
185-
*
186-
* @type {Object}
187204
*/
188205
var PRIMITIVE_TYPES = {
189206
'string': function (string) {
@@ -200,31 +217,31 @@
200217
* Convert any value to a string.
201218
*
202219
* @param {*} value
203-
* @param {String} indent
204-
* @param {Function} stringify
205-
* @return {String}
220+
* @param {string} indent
221+
* @param {Function} next
222+
* @return {string}
206223
*/
207-
var stringify = function (value, indent, stringify) {
224+
function stringify (value, indent, next) {
208225
// Convert primitives into strings.
209226
if (Object(value) !== value) {
210-
return PRIMITIVE_TYPES[typeof value](value, indent, stringify);
227+
return PRIMITIVE_TYPES[typeof value](value, indent, next);
211228
}
212229

213230
// Use the internal object string to select stringification method.
214231
var toString = OBJECT_TYPES[Object.prototype.toString.call(value)];
215232

216233
// Convert objects into strings.
217-
return toString && toString(value, indent, stringify);
218-
};
234+
return toString ? toString(value, indent, next) : undefined;
235+
}
219236

220237
/**
221238
* Stringify an object into the literal string.
222239
*
223-
* @param {Object} value
240+
* @param {*} value
224241
* @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}
228245
*/
229246
return function (value, replacer, space, options) {
230247
options = options || {}
@@ -234,48 +251,106 @@
234251
space = new Array(Math.max(0, space|0) + 1).join(' ');
235252
}
236253

237-
var maxDepth = options.maxDepth || 200;
254+
var maxDepth = Number(options.maxDepth) || 100;
255+
var references = !!options.references;
238256

239-
var depth = 0;
240-
var cache = [];
257+
var path = [];
258+
var stack = [];
259+
var encountered = [];
260+
var paths = [];
261+
var restore = [];
241262

242263
/**
243-
* Handle recursion by checking if we've visited this node every iteration.
264+
* Stringify the next value in the stack.
244265
*
245266
* @param {*} value
246-
* @return {String}
267+
* @param {string} key
268+
* @return {string}
247269
*/
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+
}
253293

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());
257302

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);
261308

262-
depth--;
263-
cache.pop();
309+
if (seen > -1 || path.length > maxDepth) {
310+
return;
311+
}
264312

265-
return result;
266-
});
267-
};
313+
stack.push(value);
314+
var value = stringify(value, space, next);
315+
stack.pop();
316+
return value;
317+
};
268318

269319
// 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`.
271321
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+
});
275330
});
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
277352
}
278353

279-
return recurse(value, stringify);
354+
return result;
280355
};
281356
});

0 commit comments

Comments
 (0)