Skip to content

Commit

Permalink
feature: implement well formed JSON.stringify behavior
Browse files Browse the repository at this point in the history
Unicode code points in the inclusive range 0xD800 to 0xDFFF are from
now on escaped as defined by the spec. This was not considered a
breaking change in the proposal and as such it's neither considered
a breaking change here.
  • Loading branch information
BridgeAR committed Oct 19, 2021
1 parent a1b24d6 commit 6e295ff
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion benchmark.js
Expand Up @@ -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)
Expand Down
30 changes: 14 additions & 16 deletions index.js
Expand Up @@ -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 = [
Expand All @@ -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)) {
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions test.js
Expand Up @@ -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()
})

0 comments on commit 6e295ff

Please sign in to comment.