Skip to content

Instantly share code, notes, and snippets.

@kalda341
Created August 17, 2019 00:53
Show Gist options
  • Select an option

  • Save kalda341/f1814d16e2503b244278205e37e64e1e to your computer and use it in GitHub Desktop.

Select an option

Save kalda341/f1814d16e2503b244278205e37e64e1e to your computer and use it in GitHub Desktop.
// As described here: http://gibson042.github.io/canonicaljson-spec/
// Probably fairly inefficient, but I really doubt it matters
export function encodeCanonicalJSON(jsonNode) {
// Escape character | quote | backslash | high surrogate not followed by low, or low surrogate not following
// a high surrogate
const replaceRegex = /[\u0000-\u001F]|"|\\|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g;
const toSlashEscape = {
"\b": "\\b",
"\t": "\\t",
"\n": "\\n",
"\f": "\\f",
"\r": "\\r",
"\"": "\\\"",
"\\": "\\\\",
};
const escapeCharacter = (character) => {
const slashEscaped = toSlashEscape[character];
if (slashEscaped) {
return slashEscaped;
} else {
// If the character is marked for replacement and we can't slash escape it, we'll encode it
// with a six-character \u00xx uppercase hexadecimal escape sequence
const charCode = character.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0");
return "\\u" + charCode;
}
}
const exponentialRegex = /^(\d)(?:\.(\d*))?e([+-]\d+)$/;
const formatExponential = (number) => {
// Must be a floating point number
const match = number.toExponential().match(exponentialRegex);
const integer = match[1];
// May not include a decimal component, but we need it
const decimal = match[2] || '0';
// Will remove + if present
const exponent = parseInt(match[3]);
return `${integer}.${decimal}E${exponent}`;
};
const doEncode = (jsonNode, depth) => {
if (depth > 1000) {
throw new Error("Max depth exceeded");
} else if (jsonNode === undefined) {
throw new Error("Invalid type: undefined");
} else if (jsonNode === null) {
return "null";
} else if (typeof jsonNode === "boolean") {
return jsonNode.toString();
} else if (typeof jsonNode === "number" && Number.isInteger(jsonNode)) {
return jsonNode.toString();
} else if (typeof jsonNode === "number") {
return formatExponential(jsonNode);
} else if (typeof jsonNode === "string") {
return `"${jsonNode.replace(replaceRegex, escapeCharacter)}"`;
} else if (typeof jsonNode === "object" && Array.isArray(jsonNode)) {
const nodeContents = jsonNode.map((value) => encodeCanonicalJSON(value, depth + 1)).join(",");
return `[${nodeContents}]`;
} else if (typeof jsonNode === "object") {
// Order the members of all objects lexicographically by the UCS (Unicode Character Set) code points of their names
// Do not include key value pairs where their value is undefined.
const nodeContents = Object.keys(jsonNode).sort().filter(
key => jsonNode[key] !== undefined
).map(key =>
[encodeCanonicalJSON(key, depth + 1), encodeCanonicalJSON(jsonNode[key], depth + 1)].join(":")
).join(",");
return `{${nodeContents}}`;
} else {
throw new Error("Unknown type");
}
};
return doEncode(jsonNode, 0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment