1
1
'use strict'
2
2
3
- const hexify = char => {
3
+ const INDENT = Symbol . for ( 'indent' )
4
+ const NEWLINE = Symbol . for ( 'newline' )
5
+
6
+ const DEFAULT_NEWLINE = '\n'
7
+ const DEFAULT_INDENT = ' '
8
+ const BOM = / ^ \uFEFF /
9
+
10
+ // only respect indentation if we got a line break, otherwise squash it
11
+ // things other than objects and arrays aren't indented, so ignore those
12
+ // Important: in both of these regexps, the $1 capture group is the newline
13
+ // or undefined, and the $2 capture group is the indent, or undefined.
14
+ const FORMAT = / ^ \s * [ { [ ] ( (?: \r ? \n ) + ) ( [ \s \t ] * ) /
15
+ const EMPTY = / ^ (?: \{ \} | \[ \] ) ( (?: \r ? \n ) + ) ? $ /
16
+
17
+ // Node 20 puts single quotes around the token and a comma after it
18
+ const UNEXPECTED_TOKEN = / ^ U n e x p e c t e d t o k e n ' ? ( .) ' ? ( , ) ? / i
19
+
20
+ const hexify = ( char ) => {
4
21
const h = char . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( )
5
- return '0x' + ( h . length % 2 ? '0' : '' ) + h
22
+ return `0x ${ h . length % 2 ? '0' : '' } ${ h } `
6
23
}
7
24
8
- const parseError = ( e , txt , context ) => {
25
+ // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
26
+ // because the buffer-to-string conversion in `fs.readFileSync()`
27
+ // translates it to FEFF, the UTF-16 BOM.
28
+ const stripBOM = ( txt ) => String ( txt ) . replace ( BOM , '' )
29
+
30
+ const makeParsedError = ( msg , parsing , position = 0 ) => ( {
31
+ message : `${ msg } while parsing ${ parsing } ` ,
32
+ position,
33
+ } )
34
+
35
+ const parseError = ( e , txt , context = 20 ) => {
36
+ let msg = e . message
37
+
9
38
if ( ! txt ) {
10
- return {
11
- message : e . message + ' while parsing empty string' ,
12
- position : 0 ,
13
- }
39
+ return makeParsedError ( msg , 'empty string' )
14
40
}
15
- const badToken = e . message . match ( / ^ U n e x p e c t e d t o k e n ( .) .* p o s i t i o n \s + ( \d + ) / i)
16
- const errIdx = badToken ? + badToken [ 2 ]
17
- : e . message . match ( / ^ U n e x p e c t e d e n d o f J S O N .* / i) ? txt . length - 1
18
- : null
19
41
20
- const msg = badToken ? e . message . replace ( / ^ U n e x p e c t e d t o k e n ./ , `Unexpected token ${
21
- JSON . stringify ( badToken [ 1 ] )
22
- } (${ hexify ( badToken [ 1 ] ) } )`)
23
- : e . message
42
+ const badTokenMatch = msg . match ( UNEXPECTED_TOKEN )
43
+ const badIndexMatch = msg . match ( / p o s i t i o n \s + ( \d + ) / i)
24
44
25
- if ( errIdx !== null && errIdx !== undefined ) {
26
- const start = errIdx <= context ? 0
27
- : errIdx - context
45
+ if ( badTokenMatch ) {
46
+ msg = msg . replace (
47
+ UNEXPECTED_TOKEN ,
48
+ `Unexpected token ${ JSON . stringify ( badTokenMatch [ 1 ] ) } (${ hexify ( badTokenMatch [ 1 ] ) } )$2 `
49
+ )
50
+ }
28
51
29
- const end = errIdx + context >= txt . length ? txt . length
30
- : errIdx + context
52
+ let errIdx
53
+ if ( badIndexMatch ) {
54
+ errIdx = + badIndexMatch [ 1 ]
55
+ } else if ( msg . match ( / ^ U n e x p e c t e d e n d o f J S O N .* / i) ) {
56
+ errIdx = txt . length - 1
57
+ }
31
58
32
- const slice = ( start === 0 ? '' : '...' ) +
33
- txt . slice ( start , end ) +
34
- ( end === txt . length ? '' : '...' )
59
+ if ( errIdx == null ) {
60
+ return makeParsedError ( msg , `' ${ txt . slice ( 0 , context * 2 ) } '` )
61
+ }
35
62
36
- const near = txt === slice ? '' : 'near '
63
+ const start = errIdx <= context ? 0 : errIdx - context
64
+ const end = errIdx + context >= txt . length ? txt . length : errIdx + context
65
+ const slice = `${ start ? '...' : '' } ${ txt . slice ( start , end ) } ${ end === txt . length ? '' : '...' } `
37
66
38
- return {
39
- message : msg + ` while parsing ${ near } ${ JSON . stringify ( slice ) } ` ,
40
- position : errIdx ,
41
- }
42
- } else {
43
- return {
44
- message : msg + ` while parsing '${ txt . slice ( 0 , context * 2 ) } '` ,
45
- position : 0 ,
46
- }
47
- }
67
+ return makeParsedError (
68
+ msg ,
69
+ `${ txt === slice ? '' : 'near ' } ${ JSON . stringify ( slice ) } ` ,
70
+ errIdx
71
+ )
48
72
}
49
73
50
74
class JSONParseError extends SyntaxError {
51
75
constructor ( er , txt , context , caller ) {
52
- context = context || 20
53
76
const metadata = parseError ( er , txt , context )
54
77
super ( metadata . message )
55
78
Object . assign ( this , metadata )
@@ -63,67 +86,50 @@ class JSONParseError extends SyntaxError {
63
86
}
64
87
65
88
set name ( n ) { }
89
+
66
90
get [ Symbol . toStringTag ] ( ) {
67
91
return this . constructor . name
68
92
}
69
93
}
70
94
71
- const kIndent = Symbol . for ( 'indent' )
72
- const kNewline = Symbol . for ( 'newline' )
73
- // only respect indentation if we got a line break, otherwise squash it
74
- // things other than objects and arrays aren't indented, so ignore those
75
- // Important: in both of these regexps, the $1 capture group is the newline
76
- // or undefined, and the $2 capture group is the indent, or undefined.
77
- const formatRE = / ^ \s * [ { [ ] ( (?: \r ? \n ) + ) ( [ \s \t ] * ) /
78
- const emptyRE = / ^ (?: \{ \} | \[ \] ) ( (?: \r ? \n ) + ) ? $ /
79
-
80
- const parseJson = ( txt , reviver , context ) => {
81
- const parseText = stripBOM ( txt )
82
- context = context || 20
83
- try {
95
+ const parseJson = ( txt , reviver ) => {
96
+ const result = JSON . parse ( txt , reviver )
97
+ if ( result && typeof result === 'object' ) {
84
98
// get the indentation so that we can save it back nicely
85
99
// if the file starts with {" then we have an indent of '', ie, none
86
- // otherwise, pick the indentation of the next line after the first \n
87
- // If the pattern doesn't match, then it means no indentation.
88
- // JSON.stringify ignores symbols, so this is reasonably safe.
89
- // if the string is '{}' or '[]', then use the default 2-space indent.
90
- const [ , newline = '\n' , indent = ' ' ] = parseText . match ( emptyRE ) ||
91
- parseText . match ( formatRE ) ||
92
- [ null , '' , '' ]
93
-
94
- const result = JSON . parse ( parseText , reviver )
95
- if ( result && typeof result === 'object' ) {
96
- result [ kNewline ] = newline
97
- result [ kIndent ] = indent
98
- }
99
- return result
100
+ // otherwise, pick the indentation of the next line after the first \n If the
101
+ // pattern doesn't match, then it means no indentation. JSON.stringify ignores
102
+ // symbols, so this is reasonably safe. if the string is '{}' or '[]', then
103
+ // use the default 2-space indent.
104
+ const match = txt . match ( EMPTY ) || txt . match ( FORMAT ) || [ null , '' , '' ]
105
+ result [ NEWLINE ] = match [ 1 ] ?? DEFAULT_NEWLINE
106
+ result [ INDENT ] = match [ 2 ] ?? DEFAULT_INDENT
107
+ }
108
+ return result
109
+ }
110
+
111
+ const parseJsonError = ( raw , reviver , context ) => {
112
+ const txt = stripBOM ( raw )
113
+ try {
114
+ return parseJson ( txt , reviver )
100
115
} catch ( e ) {
101
- if ( typeof txt !== 'string' && ! Buffer . isBuffer ( txt ) ) {
102
- const isEmptyArray = Array . isArray ( txt ) && txt . length === 0
103
- throw Object . assign ( new TypeError (
104
- `Cannot parse ${ isEmptyArray ? 'an empty array' : String ( txt ) } `
105
- ) , {
106
- code : 'EJSONPARSE' ,
107
- systemError : e ,
108
- } )
116
+ if ( typeof raw !== 'string' && ! Buffer . isBuffer ( raw ) ) {
117
+ const msg = Array . isArray ( raw ) && raw . length === 0 ? 'an empty array' : String ( raw )
118
+ throw Object . assign (
119
+ new TypeError ( `Cannot parse ${ msg } ` ) ,
120
+ { code : 'EJSONPARSE' , systemError : e }
121
+ )
109
122
}
110
-
111
- throw new JSONParseError ( e , parseText , context , parseJson )
123
+ throw new JSONParseError ( e , txt , context , parseJsonError )
112
124
}
113
125
}
114
126
115
- // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
116
- // because the buffer-to-string conversion in `fs.readFileSync()`
117
- // translates it to FEFF, the UTF-16 BOM.
118
- const stripBOM = txt => String ( txt ) . replace ( / ^ \uFEFF / , '' )
119
-
120
- module . exports = parseJson
121
- parseJson . JSONParseError = JSONParseError
122
-
123
- parseJson . noExceptions = ( txt , reviver ) => {
127
+ module . exports = parseJsonError
128
+ parseJsonError . JSONParseError = JSONParseError
129
+ parseJsonError . noExceptions = ( raw , reviver ) => {
124
130
try {
125
- return JSON . parse ( stripBOM ( txt ) , reviver )
126
- } catch ( e ) {
131
+ return parseJson ( stripBOM ( raw ) , reviver )
132
+ } catch {
127
133
// no exceptions
128
134
}
129
135
}
0 commit comments