Skip to content

Commit 6e295ff

Browse files
committed
feature: implement well formed JSON.stringify behavior
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.
1 parent a1b24d6 commit 6e295ff

File tree

5 files changed

+35
-17
lines changed

5 files changed

+35
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Added `maximumBreadth` option to limit stringification at a specific object or array "width" (number of properties / values)
66
- Added `maximumDepth` option to limit stringification at a specific nesting depth
7+
- Implemented the [well formed stringify proposal](https://github.com/tc39/proposal-well-formed-stringify) that is now part of the spec
78
- Fixed maximum spacer length (10)
89
- Fixed TypeScript definition
910
- Fixed duplicated array replacer values serialized more than once

benchmark.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const Benchmark = require('benchmark')
44
const suite = new Benchmark.Suite()
5-
const stringify = require('.')
5+
const stringify = require('.').configure({ deterministic: true })
66

77
// eslint-disable-next-line
88
const array = Array({ length: 10 }, (_, i) => i)

index.js

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ exports.configure = configure
1818
module.exports = stringify
1919

2020
// eslint-disable-next-line
21-
const strEscapeSequencesRegExp = /[\x00-\x1f\x22\x5c]/
21+
const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
2222
// eslint-disable-next-line
23-
const strEscapeSequencesReplacer = /[\x00-\x1f\x22\x5c]/g
23+
const strEscapeSequencesReplacer = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/g
2424

2525
// Escaped special characters. Use empty strings to fill up unused entries.
2626
const meta = [
@@ -40,10 +40,14 @@ const meta = [
4040
]
4141

4242
function escapeFn (str) {
43-
return meta[str.charCodeAt(0)]
43+
const charCode = str.charCodeAt(0)
44+
return meta.length > charCode
45+
? meta[charCode]
46+
: `\\u${charCode.toString(16).padStart(4, '0')}`
4447
}
4548

46-
// Escape control characters, double quotes and the backslash.
49+
// Escape C0 control characters, double quotes, the backslash and every code
50+
// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF.
4751
function strEscape (str) {
4852
// Some magic numbers that worked out fine while benchmarking with v8 8.0
4953
if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) {
@@ -54,23 +58,17 @@ function strEscape (str) {
5458
}
5559
let result = ''
5660
let last = 0
57-
let i = 0
58-
for (; i < str.length; i++) {
61+
for (let i = 0; i < str.length; i++) {
5962
const point = str.charCodeAt(i)
6063
if (point === 34 || point === 92 || point < 32) {
61-
if (last === i) {
62-
result += meta[point]
63-
} else {
64-
result += `${str.slice(last, i)}${meta[point]}`
65-
}
64+
result += `${str.slice(last, i)}${meta[point]}`
65+
last = i + 1
66+
} else if (point >= 55296 && point <= 57343) {
67+
result += `${str.slice(last, i)}${`\\u${point.toString(16).padStart(4, '0')}`}`
6668
last = i + 1
6769
}
6870
}
69-
if (last === 0) {
70-
result = str
71-
} else if (last !== i) {
72-
result += str.slice(last)
73-
}
71+
result += str.slice(last)
7472
return result
7573
}
7674

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"typings": "index.d.ts",
3939
"devDependencies": {
4040
"@types/json-stable-stringify": "^1.0.32",
41+
"@types/node": "^16.11.1",
4142
"benchmark": "^2.1.4",
4243
"clone": "^2.1.2",
4344
"fast-json-stable-stringify": "^2.1.0",

test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,21 @@ test('should throw when maximumBreadth receives malformed input', (assert) => {
995995
})
996996
assert.end()
997997
})
998+
999+
test('check for well formed stringify implementation', (assert) => {
1000+
for (let i = 0; i < 2 ** 16; i++) {
1001+
const string = String.fromCharCode(i)
1002+
const actual = stringify(string)
1003+
const expected = JSON.stringify(string)
1004+
// Older Node.js versions do not use the well formed JSON implementation.
1005+
if (Number(process.version.split('.')[0].slice(1)) >= 12 || i < 0xd800 || i > 0xdfff) {
1006+
assert.equal(actual, expected)
1007+
} else {
1008+
assert.not(actual, expected)
1009+
}
1010+
}
1011+
// Trigger special case
1012+
const longStringEscape = stringify(`${'a'.repeat(100)}\uD800`)
1013+
assert.equal(longStringEscape, `"${'a'.repeat(100)}\\ud800"`)
1014+
assert.end()
1015+
})

0 commit comments

Comments
 (0)