189 lines
6.2 KiB
JavaScript
189 lines
6.2 KiB
JavaScript
|
/*!
|
||
|
Copyright (C) 2013 by WebReflection
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
of this software and associated documentation files (the "Software"), to deal
|
||
|
in the Software without restriction, including without limitation the rights
|
||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
copies of the Software, and to permit persons to whom the Software is
|
||
|
furnished to do so, subject to the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be included in
|
||
|
all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
THE SOFTWARE.
|
||
|
|
||
|
*/
|
||
|
var CircularJSON = (function(JSON, RegExp){
|
||
|
var
|
||
|
// should be a not so common char
|
||
|
// possibly one JSON does not encode
|
||
|
// possibly one encodeURIComponent does not encode
|
||
|
// right now this char is '~' but this might change in the future
|
||
|
specialChar = '~',
|
||
|
safeSpecialChar = '\\x' + (
|
||
|
'0' + specialChar.charCodeAt(0).toString(16)
|
||
|
).slice(-2),
|
||
|
escapedSafeSpecialChar = '\\' + safeSpecialChar,
|
||
|
specialCharRG = new RegExp(safeSpecialChar, 'g'),
|
||
|
safeSpecialCharRG = new RegExp(escapedSafeSpecialChar, 'g'),
|
||
|
|
||
|
safeStartWithSpecialCharRG = new RegExp('(?:^|([^\\\\]))' + escapedSafeSpecialChar),
|
||
|
|
||
|
indexOf = [].indexOf || function(v){
|
||
|
for(var i=this.length;i--&&this[i]!==v;);
|
||
|
return i;
|
||
|
},
|
||
|
$String = String // there's no way to drop warnings in JSHint
|
||
|
// about new String ... well, I need that here!
|
||
|
// faked, and happy linter!
|
||
|
;
|
||
|
|
||
|
function generateReplacer(value, replacer, resolve) {
|
||
|
var
|
||
|
path = [],
|
||
|
all = [value],
|
||
|
seen = [value],
|
||
|
mapp = [resolve ? specialChar : '[Circular]'],
|
||
|
last = value,
|
||
|
lvl = 1,
|
||
|
i
|
||
|
;
|
||
|
return function(key, value) {
|
||
|
// the replacer has rights to decide
|
||
|
// if a new object should be returned
|
||
|
// or if there's some key to drop
|
||
|
// let's call it here rather than "too late"
|
||
|
if (replacer) value = replacer.call(this, key, value);
|
||
|
|
||
|
// did you know ? Safari passes keys as integers for arrays
|
||
|
// which means if (key) when key === 0 won't pass the check
|
||
|
if (key !== '') {
|
||
|
if (last !== this) {
|
||
|
i = lvl - indexOf.call(all, this) - 1;
|
||
|
lvl -= i;
|
||
|
all.splice(lvl, all.length);
|
||
|
path.splice(lvl - 1, path.length);
|
||
|
last = this;
|
||
|
}
|
||
|
// console.log(lvl, key, path);
|
||
|
if (typeof value === 'object' && value) {
|
||
|
// if object isn't referring to parent object, add to the
|
||
|
// object path stack. Otherwise it is already there.
|
||
|
if (indexOf.call(all, value) < 0) {
|
||
|
all.push(last = value);
|
||
|
}
|
||
|
lvl = all.length;
|
||
|
i = indexOf.call(seen, value);
|
||
|
if (i < 0) {
|
||
|
i = seen.push(value) - 1;
|
||
|
if (resolve) {
|
||
|
// key cannot contain specialChar but could be not a string
|
||
|
path.push(('' + key).replace(specialCharRG, safeSpecialChar));
|
||
|
mapp[i] = specialChar + path.join(specialChar);
|
||
|
} else {
|
||
|
mapp[i] = mapp[0];
|
||
|
}
|
||
|
} else {
|
||
|
value = mapp[i];
|
||
|
}
|
||
|
} else {
|
||
|
if (typeof value === 'string' && resolve) {
|
||
|
// ensure no special char involved on deserialization
|
||
|
// in this case only first char is important
|
||
|
// no need to replace all value (better performance)
|
||
|
value = value .replace(safeSpecialChar, escapedSafeSpecialChar)
|
||
|
.replace(specialChar, safeSpecialChar);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return value;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function retrieveFromPath(current, keys) {
|
||
|
for(var i = 0, length = keys.length; i < length; current = current[
|
||
|
// keys should be normalized back here
|
||
|
keys[i++].replace(safeSpecialCharRG, specialChar)
|
||
|
]);
|
||
|
return current;
|
||
|
}
|
||
|
|
||
|
function generateReviver(reviver) {
|
||
|
return function(key, value) {
|
||
|
var isString = typeof value === 'string';
|
||
|
if (isString && value.charAt(0) === specialChar) {
|
||
|
return new $String(value.slice(1));
|
||
|
}
|
||
|
if (key === '') value = regenerate(value, value, {});
|
||
|
// again, only one needed, do not use the RegExp for this replacement
|
||
|
// only keys need the RegExp
|
||
|
if (isString) value = value .replace(safeStartWithSpecialCharRG, '$1' + specialChar)
|
||
|
.replace(escapedSafeSpecialChar, safeSpecialChar);
|
||
|
return reviver ? reviver.call(this, key, value) : value;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function regenerateArray(root, current, retrieve) {
|
||
|
for (var i = 0, length = current.length; i < length; i++) {
|
||
|
current[i] = regenerate(root, current[i], retrieve);
|
||
|
}
|
||
|
return current;
|
||
|
}
|
||
|
|
||
|
function regenerateObject(root, current, retrieve) {
|
||
|
for (var key in current) {
|
||
|
if (current.hasOwnProperty(key)) {
|
||
|
current[key] = regenerate(root, current[key], retrieve);
|
||
|
}
|
||
|
}
|
||
|
return current;
|
||
|
}
|
||
|
|
||
|
function regenerate(root, current, retrieve) {
|
||
|
return current instanceof Array ?
|
||
|
// fast Array reconstruction
|
||
|
regenerateArray(root, current, retrieve) :
|
||
|
(
|
||
|
current instanceof $String ?
|
||
|
(
|
||
|
// root is an empty string
|
||
|
current.length ?
|
||
|
(
|
||
|
retrieve.hasOwnProperty(current) ?
|
||
|
retrieve[current] :
|
||
|
retrieve[current] = retrieveFromPath(
|
||
|
root, current.split(specialChar)
|
||
|
)
|
||
|
) :
|
||
|
root
|
||
|
) :
|
||
|
(
|
||
|
current instanceof Object ?
|
||
|
// dedicated Object parser
|
||
|
regenerateObject(root, current, retrieve) :
|
||
|
// value as it is
|
||
|
current
|
||
|
)
|
||
|
)
|
||
|
;
|
||
|
}
|
||
|
|
||
|
function stringifyRecursion(value, replacer, space, doNotResolve) {
|
||
|
return JSON.stringify(value, generateReplacer(value, replacer, !doNotResolve), space);
|
||
|
}
|
||
|
|
||
|
function parseRecursion(text, reviver) {
|
||
|
return JSON.parse(text, generateReviver(reviver));
|
||
|
}
|
||
|
return {
|
||
|
stringify: stringifyRecursion,
|
||
|
parse: parseRecursion
|
||
|
};
|
||
|
}(JSON, RegExp));
|