Last active
February 20, 2020 16:49
-
-
Save agrublev/caaf7618a46cff433d536c1548aa5ed0 to your computer and use it in GitHub Desktop.
Circular JSON stringify
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 doNotIgnore = false, | |
inspect = !!replacer, | |
path = [], | |
all = [value], | |
seen = [value], | |
mapp = [resolve ? specialChar : "[Circular]"], | |
last = value, | |
lvl = 1, | |
i, | |
fn; | |
if (inspect) { | |
fn = | |
typeof replacer === "object" | |
? function(key, value) { | |
return key !== "" && indexOf.call(replacer, key) < 0 ? void 0 : value; | |
} | |
: replacer; | |
} | |
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 (inspect) value = fn.call(this, key, value); | |
// first pass should be ignored, since it's just the initial object | |
if (doNotIgnore) { | |
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); | |
} | |
} | |
} else { | |
doNotIgnore = true; | |
} | |
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; | |
} | |
const CircularJSON = { | |
stringify: function stringify(value, replacer, space, doNotResolve) { | |
return CircularJSON.parser.stringify( | |
value, | |
generateReplacer(value, replacer, !doNotResolve), | |
space | |
); | |
}, | |
parse: function parse(text, reviver) { | |
return CircularJSON.parser.parse(text, generateReviver(reviver)); | |
}, | |
// A parser should be an API 1:1 compatible with JSON | |
// it should expose stringify and parse methods. | |
// The default parser is the native JSON. | |
parser: JSON | |
}; | |
export default CircularJSON; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment