Skip to content

Commit 0fc4067

Browse files
authored
Merge pull request #17 from Rich-Harris/gh-13
Handle lone surrogates
2 parents c1d70da + 9463b0f commit 0fc4067

File tree

2 files changed

+58
-9
lines changed

2 files changed

+58
-9
lines changed

src/index.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
22
const reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
3-
const unsafe = /[<>\/\u2028\u2029]/g;
4-
const escaped: Record<string, string> = { '<': '\\u003C', '>' : '\\u003E', '/': '\\u002F', '\u2028': '\\u2028', '\u2029': '\\u2029' };
3+
const escaped: Record<string, string> = {
4+
'<': '\\u003C',
5+
'>' : '\\u003E',
6+
'/': '\\u002F',
7+
'\\': '\\\\',
8+
'\b': '\\b',
9+
'\f': '\\f',
10+
'\n': '\\n',
11+
'\r': '\\r',
12+
'\t': '\\t',
13+
'\0': '\\0',
14+
'\u2028': '\\u2028',
15+
'\u2029': '\\u2029'
16+
};
517
const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0');
618

719
export default function devalue(value: any) {
820
const counts = new Map();
921

10-
let n = 0;
11-
1222
function walk(thing: any) {
1323
if (typeof thing === 'function') {
1424
throw new Error(`Cannot stringify a function`);
@@ -197,12 +207,8 @@ function isPrimitive(thing: any) {
197207
return Object(thing) !== thing;
198208
}
199209

200-
function escape(char: string) {
201-
return escaped[char];
202-
}
203-
204210
function stringifyPrimitive(thing: any) {
205-
if (typeof thing === 'string') return JSON.stringify(thing).replace(unsafe, escape);
211+
if (typeof thing === 'string') return stringifyString(thing);
206212
if (thing === void 0) return 'void 0';
207213
if (thing === 0 && 1 / thing < 0) return '-0';
208214
const str = String(thing);
@@ -220,4 +226,34 @@ function safeKey(key: string) {
220226

221227
function safeProp(key: string) {
222228
return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
229+
}
230+
231+
function stringifyString(str: string) {
232+
let result = '"';
233+
234+
for (let i = 0; i < str.length; i += 1) {
235+
const char = str.charAt(i);
236+
const code = char.charCodeAt(0);
237+
238+
if (char === '"') {
239+
result += '\\"';
240+
} else if (char in escaped) {
241+
result += escaped[char];
242+
} else if (code >= 0xd800 && code <= 0xdfff) {
243+
const next = str.charCodeAt(i + 1);
244+
245+
// If this is the beginning of a [high, low] surrogate pair,
246+
// add the next two characters, otherwise escape
247+
if (code <= 0xdbff && (next >= 0xdc00 && next <= 0xdfff)) {
248+
result += char + str[++i];
249+
} else {
250+
result += `\\u${code.toString(16).toUpperCase()}`;
251+
}
252+
} else {
253+
result += char;
254+
}
255+
}
256+
257+
result += '"';
258+
return result;
223259
}

test/test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ describe('devalue', () => {
3535
test('Map', new Map([['a', 'b']]), 'new Map([["a","b"]])');
3636
});
3737

38+
describe('strings', () => {
39+
test('newline', 'a\nb', JSON.stringify('a\nb'));
40+
test('double quotes', '"yar"', JSON.stringify('"yar"'));
41+
test('lone low surrogate', "a\uDC00b", '"a\\uDC00b"');
42+
test('lone high surrogate', "a\uD800b", '"a\\uD800b"');
43+
test('two low surrogates', "a\uDC00\uDC00b", '"a\\uDC00\\uDC00b"');
44+
test('two high surrogates', "a\uD800\uD800b", '"a\\uD800\\uD800b"');
45+
test('surrogate pair', '𝌆', JSON.stringify('𝌆'));
46+
test('surrogate pair in wrong order', 'a\uDC00\uD800b', '"a\\uDC00\\uD800b"');
47+
test('nul', '\0', '"\\0"');
48+
test('backslash', '\\', JSON.stringify('\\'));
49+
});
50+
3851
describe('cycles', () => {
3952
let map = new Map();
4053
map.set('self', map);

0 commit comments

Comments
 (0)