UNPKG

7.33 kBJavaScriptView Raw
1// JSON formatting
2
3const esprima = require('esprima')
4
5const {
6 CommentArray,
7} = require('./array')
8
9const {
10 NON_PROP_SYMBOL_KEYS,
11
12 PREFIX_BEFORE,
13 PREFIX_AFTER_PROP,
14 PREFIX_AFTER_COLON,
15 PREFIX_AFTER_VALUE,
16 PREFIX_AFTER,
17
18 PREFIX_BEFORE_ALL,
19 PREFIX_AFTER_ALL,
20
21 BRACKET_OPEN,
22 BRACKET_CLOSE,
23 CURLY_BRACKET_OPEN,
24 CURLY_BRACKET_CLOSE,
25
26 COLON,
27 COMMA,
28 MINUS,
29 EMPTY,
30
31 UNDEFINED,
32
33 define
34} = require('./common')
35
36const tokenize = code => esprima.tokenize(code, {
37 comment: true,
38 loc: true
39})
40
41const previous_hosts = []
42let comments_host = null
43let unassigned_comments = null
44
45const previous_props = []
46let last_prop
47
48let remove_comments = false
49let inline = false
50let tokens = null
51let last = null
52let current = null
53let index
54let reviver = null
55
56const clean = () => {
57 previous_props.length =
58 previous_hosts.length = 0
59
60 last = null
61 last_prop = UNDEFINED
62}
63
64const free = () => {
65 clean()
66
67 tokens.length = 0
68
69 unassigned_comments =
70 comments_host =
71 tokens =
72 last =
73 current =
74 reviver = null
75}
76
77const symbolFor = prefix => Symbol.for(
78 last_prop !== UNDEFINED
79 ? prefix + COLON + last_prop
80 : prefix
81)
82
83const transform = (k, v) => reviver
84 ? reviver(k, v)
85 : v
86
87const unexpected = () => {
88 const error = new SyntaxError(`Unexpected token ${current.value.slice(0, 1)}`)
89 Object.assign(error, current.loc.start)
90
91 throw error
92}
93
94const unexpected_end = () => {
95 const error = new SyntaxError('Unexpected end of JSON input')
96 Object.assign(error, last
97 ? last.loc.end
98 // Empty string
99 : {
100 line: 1,
101 column: 0
102 })
103
104 throw error
105}
106
107// Move the reader to the next
108const next = () => {
109 const new_token = tokens[++ index]
110 inline = current
111 && new_token
112 && current.loc.end.line === new_token.loc.start.line
113 || false
114
115 last = current
116 current = new_token
117}
118
119const type = () => {
120 if (!current) {
121 unexpected_end()
122 }
123
124 return current.type === 'Punctuator'
125 ? current.value
126 : current.type
127}
128
129const is = t => type() === t
130
131const expect = a => {
132 if (!is(a)) {
133 unexpected()
134 }
135}
136
137const set_comments_host = new_host => {
138 previous_hosts.push(comments_host)
139 comments_host = new_host
140}
141
142const restore_comments_host = () => {
143 comments_host = previous_hosts.pop()
144}
145
146const assign_after_comments = () => {
147 if (!unassigned_comments) {
148 return
149 }
150
151 const after_comments = []
152
153 for (const comment of unassigned_comments) {
154 // If the comment is inline, then it is an after-comma comment
155 if (comment.inline) {
156 after_comments.push(comment)
157 // Otherwise, all comments are before:<next-prop> comment
158 } else {
159 break
160 }
161 }
162
163 const {length} = after_comments
164 if (!length) {
165 return
166 }
167
168 if (length === unassigned_comments.length) {
169 // If unassigned_comments are all consumed
170 unassigned_comments = null
171 } else {
172 unassigned_comments.splice(0, length)
173 }
174
175 define(comments_host, symbolFor(PREFIX_AFTER), after_comments)
176}
177
178const assign_comments = prefix => {
179 if (!unassigned_comments) {
180 return
181 }
182
183 define(comments_host, symbolFor(prefix), unassigned_comments)
184
185 unassigned_comments = null
186}
187
188const parse_comments = prefix => {
189 const comments = []
190
191 while (
192 current
193 && (
194 is('LineComment')
195 || is('BlockComment')
196 )
197 ) {
198 const comment = {
199 ...current,
200 inline
201 }
202
203 // delete comment.loc
204 comments.push(comment)
205
206 next()
207 }
208
209 if (remove_comments) {
210 return
211 }
212
213 if (!comments.length) {
214 return
215 }
216
217 if (prefix) {
218 define(comments_host, symbolFor(prefix), comments)
219 return
220 }
221
222 unassigned_comments = comments
223}
224
225const set_prop = (prop, push) => {
226 if (push) {
227 previous_props.push(last_prop)
228 }
229
230 last_prop = prop
231}
232
233const restore_prop = () => {
234 last_prop = previous_props.pop()
235}
236
237const parse_object = () => {
238 const obj = {}
239 set_comments_host(obj)
240 set_prop(UNDEFINED, true)
241
242 let started = false
243 let name
244
245 parse_comments()
246
247 while (!is(CURLY_BRACKET_CLOSE)) {
248 if (started) {
249 assign_comments(PREFIX_AFTER_VALUE)
250
251 // key-value pair delimiter
252 expect(COMMA)
253 next()
254 parse_comments()
255
256 assign_after_comments()
257
258 // If there is a trailing comma, we might reach the end
259 // ```
260 // {
261 // "a": 1,
262 // }
263 // ```
264 if (is(CURLY_BRACKET_CLOSE)) {
265 break
266 }
267 }
268
269 started = true
270 expect('String')
271 name = JSON.parse(current.value)
272
273 set_prop(name)
274 assign_comments(PREFIX_BEFORE)
275
276 next()
277 parse_comments(PREFIX_AFTER_PROP)
278
279 expect(COLON)
280
281 next()
282 parse_comments(PREFIX_AFTER_COLON)
283
284 obj[name] = transform(name, walk())
285 parse_comments()
286 }
287
288 if (started) {
289 // If there are properties,
290 // then the unassigned comments are after comments
291 assign_comments(PREFIX_AFTER)
292 }
293
294 // bypass }
295 next()
296 last_prop = undefined
297
298 if (!started) {
299 // Otherwise, they are before comments
300 assign_comments(PREFIX_BEFORE)
301 }
302
303 restore_comments_host()
304 restore_prop()
305
306 return obj
307}
308
309const parse_array = () => {
310 const array = new CommentArray()
311 set_comments_host(array)
312 set_prop(UNDEFINED, true)
313
314 let started = false
315 let i = 0
316
317 parse_comments()
318
319 while (!is(BRACKET_CLOSE)) {
320 if (started) {
321 assign_comments(PREFIX_AFTER_VALUE)
322 expect(COMMA)
323 next()
324 parse_comments()
325
326 assign_after_comments()
327
328 if (is(BRACKET_CLOSE)) {
329 break
330 }
331 }
332
333 started = true
334
335 set_prop(i)
336 assign_comments(PREFIX_BEFORE)
337
338 array[i] = transform(i, walk())
339 i ++
340
341 parse_comments()
342 }
343
344 if (started) {
345 assign_comments(PREFIX_AFTER)
346 }
347
348 next()
349 last_prop = undefined
350
351 if (!started) {
352 assign_comments(PREFIX_BEFORE)
353 }
354
355 restore_comments_host()
356 restore_prop()
357
358 return array
359}
360
361function walk () {
362 let tt = type()
363
364 if (tt === CURLY_BRACKET_OPEN) {
365 next()
366 return parse_object()
367 }
368
369 if (tt === BRACKET_OPEN) {
370 next()
371 return parse_array()
372 }
373
374 let negative = EMPTY
375
376 // -1
377 if (tt === MINUS) {
378 next()
379 tt = type()
380 negative = MINUS
381 }
382
383 let v
384
385 switch (tt) {
386 case 'String':
387 case 'Boolean':
388 case 'Null':
389 case 'Numeric':
390 v = current.value
391 next()
392 return JSON.parse(negative + v)
393 default:
394 }
395}
396
397const isObject = subject => Object(subject) === subject
398
399const parse = (code, rev, no_comments) => {
400 // Clean variables in closure
401 clean()
402
403 tokens = tokenize(code)
404 reviver = rev
405 remove_comments = no_comments
406
407 if (!tokens.length) {
408 unexpected_end()
409 }
410
411 index = - 1
412 next()
413
414 set_comments_host({})
415
416 parse_comments(PREFIX_BEFORE_ALL)
417
418 let result = walk()
419
420 parse_comments(PREFIX_AFTER_ALL)
421
422 if (current) {
423 unexpected()
424 }
425
426 if (!no_comments && result !== null) {
427 if (!isObject(result)) {
428 // 1 -> new Number(1)
429 // true -> new Boolean(1)
430 // "foo" -> new String("foo")
431
432 // eslint-disable-next-line no-new-object
433 result = new Object(result)
434 }
435
436 NON_PROP_SYMBOL_KEYS.forEach(key => {
437 const comments = comments_host[key]
438
439 if (comments) {
440 define(result, key, comments)
441 }
442 })
443 }
444
445 restore_comments_host()
446
447 // reviver
448 result = transform('', result)
449
450 free()
451
452 return result
453}
454
455module.exports = {
456 parse,
457 tokenize
458}