Skip to content

Instantly share code, notes, and snippets.

@agrublev
Last active February 20, 2020 16:49
Show Gist options
  • Save agrublev/caaf7618a46cff433d536c1548aa5ed0 to your computer and use it in GitHub Desktop.
Save agrublev/caaf7618a46cff433d536c1548aa5ed0 to your computer and use it in GitHub Desktop.
Circular JSON stringify
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