// RFC 8259 calls the following "two character sequence escape representations of some popular characters" const popularCharsEscapes = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\' }; // Escape a single character to be used in a JSON string // We follow RFC 8259 suggestions and produce the shortest possible escape sequence: // * for "popular characters", we use the two character sequence escape representation // * for other characters, we use the \uXXXX numeric escape representation function escapeChar(char: string): string { const popularEscape = popularCharsEscapes[char]; if (popularEscape !== undefined) { // We found a "popular character", use the short two character escape sequence return popularEscape; } else { // All other characters should be represented as a numeric \uXXXX escape // We are in the Basic Multilingual Plane, so `.charCodeAt` will return the same value as `.codePointAt` // We use `.charCodeAt` because it is faster const codePoint = char.charCodeAt(0); return '\\u' + codePoint.toString(16).padStart(4, '0'); } } // The set of characters that need to be escaped in a JSON string // We only escape the characters that need to be escaped according to RFC 8259 // This consists of: // * C0 control characters (U+0000 through U+001F) // * quotation mark (U+0022) and reverse solidus (U+005C), which are special characters in JSON // * surrogate code points (U+D800 through U+DFFF), which should only match when unpaired, which is are actually invalid in unicode, but RFC 8259 suggests to escape them when they appear because of errors // We use the `u` flag to match surrogates only when they are unpaired // Note: this is one of the rare occasions where we do want control characters in a regex, so we disable the eslint rule // eslint-disable-next-line no-control-regex const charsToEscape = /[\u0000-\u001f\ud800-\udfff"\\]/gu; function JSONStringifyString(str: string) { return `"` + str.replace(charsToEscape, escapeChar) + `"`; } function isIndexKey(key: string): boolean { // EcmaScript defines an index key as a string that is a base-10 unsigned integer // that does not have a leading zero or is '0' return key === '0' || /^[1-9]\d*$/.test(key); } function compareObjectKeys(a: string, b: string): number { const aIsIndexKey = isIndexKey(a); const bIsIndexKey = isIndexKey(b); // follow EcmaScript for sorting keys that could be interpreted as indices: // sort keys that could be interpreted as indices before other keys // these keys are sorted by numeric value return aIsIndexKey && bIsIndexKey ? Math.sign(Number(a) - Number(b)) : aIsIndexKey ? -1 : bIsIndexKey ? 1 : a < b ? -1 : a > b ? 1 : 0; } // adapted from https://github.com/epoberezkin/fast-json-stable-stringify // export so that it can be used in tests export function stableJSONStringify_portable(data: unknown): string | undefined { // track the objects being serialized, so that we don't get into an infinite cycle const seen = new Set(); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (function stringify(node: any) { if (typeof node?.toJSON === 'function') { node = node.toJSON(); } if (typeof node == 'number') return isFinite(node) ? '' + node : 'null'; if (typeof node === 'string') return JSONStringifyString(node); if (typeof node === 'boolean') return `${node}`; if (node === null) return 'null'; if (typeof node === 'object') { if (seen.has(node)) { throw new TypeError('Converting circular structure to JSON'); } seen.add(node); if (Array.isArray(node)) { let out = '['; for (let idx = 0; idx < node.length; idx++) { if (idx > 0) out += ','; out += stringify(node[idx]) ?? 'null'; } seen.delete(node); return out + ']'; } else { const keys = Object.keys(node).sort(compareObjectKeys); let out = ''; for (const key of keys) { const value = stringify(node[key]); if (!value) continue; if (out) out += ','; out += JSONStringifyString(key) + ':' + value; } seen.delete(node); return '{' + out + '}'; } } if (typeof node === 'bigint') { // The ECMAScript JSON.stringify function cannot serialize BigInts throw new TypeError('Do not know how to serialize a BigInt'); } if (node === undefined || typeof node === 'function') return; })(data); }