import { JSONValue } from "./types"; // As described here: http://gibson042.github.io/canonicaljson-spec/ // Probably fairly inefficient, but I really doubt it matters export function encodeCanonicalJSON(jsonNode: JSONValue): string { // Escape character | quote | backslash | high surrogate not followed by low, or low surrogate not following // a high surrogate // eslint-disable-next-line no-control-regex const replaceRegex = /[\u0000-\u001F]|"|\\|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g; const toSlashEscape: { [member: string]: string | undefined } = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", "\r": "\\r", '"': '\\"', "\\": "\\\\" }; const escapeCharacter = (character: string): string => { 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 doEncode = (jsonNode: JSONValue, depth: number): string => { if (depth > 100) { throw new Error( "Max depth exceeded. Structure is probably recursive." ); } 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") { const exponent = Math.floor(Math.log10(Math.abs(jsonNode))); const mantissa = jsonNode / Math.pow(10, exponent); // Ensure decimal point exists const mantissaString = Number.isInteger(mantissa) ? `${mantissa}.0` : mantissa.toString(); return `${mantissaString}E${exponent}`; } else if (typeof jsonNode === "string") { return `"${jsonNode.replace(replaceRegex, escapeCharacter)}"`; } else if (typeof jsonNode === "object" && Array.isArray(jsonNode)) { const nodeContents = jsonNode .map(value => doEncode(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 => [ doEncode(key, depth + 1), doEncode(jsonNode[key], depth + 1) ].join(":") ) .join(","); return `{${nodeContents}}`; } else { throw new Error("Unknown type"); } }; return doEncode(jsonNode, 0); }