diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc22d3..c7eb2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added `maximumBreadth` option to limit stringification at a specific object or array "width" (number of properties / values) - Added `maximumDepth` option to limit stringification at a specific nesting depth +- Implemented the [well formed stringify proposal](https://github.com/tc39/proposal-well-formed-stringify) that is now part of the spec - Fixed maximum spacer length (10) - Fixed TypeScript definition - Fixed duplicated array replacer values serialized more than once diff --git a/benchmark.js b/benchmark.js index 9e29749..c9464a6 100644 --- a/benchmark.js +++ b/benchmark.js @@ -2,7 +2,7 @@ const Benchmark = require('benchmark') const suite = new Benchmark.Suite() -const stringify = require('.') +const stringify = require('.').configure({ deterministic: true }) // eslint-disable-next-line const array = Array({ length: 10 }, (_, i) => i) diff --git a/index.js b/index.js index 532b784..a0856e1 100644 --- a/index.js +++ b/index.js @@ -18,9 +18,9 @@ exports.configure = configure module.exports = stringify // eslint-disable-next-line -const strEscapeSequencesRegExp = /[\x00-\x1f\x22\x5c]/ +const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ // eslint-disable-next-line -const strEscapeSequencesReplacer = /[\x00-\x1f\x22\x5c]/g +const strEscapeSequencesReplacer = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/g // Escaped special characters. Use empty strings to fill up unused entries. const meta = [ @@ -40,10 +40,14 @@ const meta = [ ] function escapeFn (str) { - return meta[str.charCodeAt(0)] + const charCode = str.charCodeAt(0) + return meta.length > charCode + ? meta[charCode] + : `\\u${charCode.toString(16).padStart(4, '0')}` } -// Escape control characters, double quotes and the backslash. +// Escape C0 control characters, double quotes, the backslash and every code +// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF. function strEscape (str) { // Some magic numbers that worked out fine while benchmarking with v8 8.0 if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) { @@ -54,23 +58,17 @@ function strEscape (str) { } let result = '' let last = 0 - let i = 0 - for (; i < str.length; i++) { + for (let i = 0; i < str.length; i++) { const point = str.charCodeAt(i) if (point === 34 || point === 92 || point < 32) { - if (last === i) { - result += meta[point] - } else { - result += `${str.slice(last, i)}${meta[point]}` - } + result += `${str.slice(last, i)}${meta[point]}` + last = i + 1 + } else if (point >= 55296 && point <= 57343) { + result += `${str.slice(last, i)}${`\\u${point.toString(16).padStart(4, '0')}`}` last = i + 1 } } - if (last === 0) { - result = str - } else if (last !== i) { - result += str.slice(last) - } + result += str.slice(last) return result } diff --git a/package.json b/package.json index b1885ba..23d14bd 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "typings": "index.d.ts", "devDependencies": { "@types/json-stable-stringify": "^1.0.32", + "@types/node": "^16.11.1", "benchmark": "^2.1.4", "clone": "^2.1.2", "fast-json-stable-stringify": "^2.1.0", diff --git a/test.js b/test.js index 0aebbad..10bb7be 100644 --- a/test.js +++ b/test.js @@ -995,3 +995,21 @@ test('should throw when maximumBreadth receives malformed input', (assert) => { }) assert.end() }) + +test('check for well formed stringify implementation', (assert) => { + for (let i = 0; i < 2 ** 16; i++) { + const string = String.fromCharCode(i) + const actual = stringify(string) + const expected = JSON.stringify(string) + // Older Node.js versions do not use the well formed JSON implementation. + if (Number(process.version.split('.')[0].slice(1)) >= 12 || i < 0xd800 || i > 0xdfff) { + assert.equal(actual, expected) + } else { + assert.not(actual, expected) + } + } + // Trigger special case + const longStringEscape = stringify(`${'a'.repeat(100)}\uD800`) + assert.equal(longStringEscape, `"${'a'.repeat(100)}\\ud800"`) + assert.end() +})