1 |
|
2 |
|
3 | const esprima = require('esprima')
|
4 |
|
5 | const {
|
6 | CommentArray,
|
7 | } = require('./array')
|
8 |
|
9 | const {
|
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 |
|
36 | const tokenize = code => esprima.tokenize(code, {
|
37 | comment: true,
|
38 | loc: true
|
39 | })
|
40 |
|
41 | const previous_hosts = []
|
42 | let comments_host = null
|
43 | let unassigned_comments = null
|
44 |
|
45 | const previous_props = []
|
46 | let last_prop
|
47 |
|
48 | let remove_comments = false
|
49 | let inline = false
|
50 | let tokens = null
|
51 | let last = null
|
52 | let current = null
|
53 | let index
|
54 | let reviver = null
|
55 |
|
56 | const clean = () => {
|
57 | previous_props.length =
|
58 | previous_hosts.length = 0
|
59 |
|
60 | last = null
|
61 | last_prop = UNDEFINED
|
62 | }
|
63 |
|
64 | const 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 |
|
77 | const symbolFor = prefix => Symbol.for(
|
78 | last_prop !== UNDEFINED
|
79 | ? prefix + COLON + last_prop
|
80 | : prefix
|
81 | )
|
82 |
|
83 | const transform = (k, v) => reviver
|
84 | ? reviver(k, v)
|
85 | : v
|
86 |
|
87 | const 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 |
|
94 | const unexpected_end = () => {
|
95 | const error = new SyntaxError('Unexpected end of JSON input')
|
96 | Object.assign(error, last
|
97 | ? last.loc.end
|
98 |
|
99 | : {
|
100 | line: 1,
|
101 | column: 0
|
102 | })
|
103 |
|
104 | throw error
|
105 | }
|
106 |
|
107 |
|
108 | const 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 |
|
119 | const type = () => {
|
120 | if (!current) {
|
121 | unexpected_end()
|
122 | }
|
123 |
|
124 | return current.type === 'Punctuator'
|
125 | ? current.value
|
126 | : current.type
|
127 | }
|
128 |
|
129 | const is = t => type() === t
|
130 |
|
131 | const expect = a => {
|
132 | if (!is(a)) {
|
133 | unexpected()
|
134 | }
|
135 | }
|
136 |
|
137 | const set_comments_host = new_host => {
|
138 | previous_hosts.push(comments_host)
|
139 | comments_host = new_host
|
140 | }
|
141 |
|
142 | const restore_comments_host = () => {
|
143 | comments_host = previous_hosts.pop()
|
144 | }
|
145 |
|
146 | const assign_after_comments = () => {
|
147 | if (!unassigned_comments) {
|
148 | return
|
149 | }
|
150 |
|
151 | const after_comments = []
|
152 |
|
153 | for (const comment of unassigned_comments) {
|
154 |
|
155 | if (comment.inline) {
|
156 | after_comments.push(comment)
|
157 |
|
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 |
|
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 |
|
178 | const 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 |
|
188 | const 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 |
|
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 |
|
225 | const set_prop = (prop, push) => {
|
226 | if (push) {
|
227 | previous_props.push(last_prop)
|
228 | }
|
229 |
|
230 | last_prop = prop
|
231 | }
|
232 |
|
233 | const restore_prop = () => {
|
234 | last_prop = previous_props.pop()
|
235 | }
|
236 |
|
237 | const 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 |
|
252 | expect(COMMA)
|
253 | next()
|
254 | parse_comments()
|
255 |
|
256 | assign_after_comments()
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
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 |
|
290 |
|
291 | assign_comments(PREFIX_AFTER)
|
292 | }
|
293 |
|
294 |
|
295 | next()
|
296 | last_prop = undefined
|
297 |
|
298 | if (!started) {
|
299 |
|
300 | assign_comments(PREFIX_BEFORE)
|
301 | }
|
302 |
|
303 | restore_comments_host()
|
304 | restore_prop()
|
305 |
|
306 | return obj
|
307 | }
|
308 |
|
309 | const 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 |
|
361 | function 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 |
|
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 |
|
397 | const isObject = subject => Object(subject) === subject
|
398 |
|
399 | const parse = (code, rev, no_comments) => {
|
400 |
|
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 |
|
429 |
|
430 |
|
431 |
|
432 |
|
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 |
|
448 | result = transform('', result)
|
449 |
|
450 | free()
|
451 |
|
452 | return result
|
453 | }
|
454 |
|
455 | module.exports = {
|
456 | parse,
|
457 | tokenize
|
458 | }
|