UNPKG

14.7 kBJavaScriptView Raw
1'use strict'
2
3let Declaration = require('./declaration')
4let tokenizer = require('./tokenize')
5let Comment = require('./comment')
6let AtRule = require('./at-rule')
7let Root = require('./root')
8let Rule = require('./rule')
9
10const SAFE_COMMENT_NEIGHBOR = {
11 empty: true,
12 space: true
13}
14
15function findLastWithPosition(tokens) {
16 for (let i = tokens.length - 1; i >= 0; i--) {
17 let token = tokens[i]
18 let pos = token[3] || token[2]
19 if (pos) return pos
20 }
21}
22
23class Parser {
24 constructor(input) {
25 this.input = input
26
27 this.root = new Root()
28 this.current = this.root
29 this.spaces = ''
30 this.semicolon = false
31
32 this.createTokenizer()
33 this.root.source = { input, start: { column: 1, line: 1, offset: 0 } }
34 }
35
36 atrule(token) {
37 let node = new AtRule()
38 node.name = token[1].slice(1)
39 if (node.name === '') {
40 this.unnamedAtrule(node, token)
41 }
42 this.init(node, token[2])
43
44 let type
45 let prev
46 let shift
47 let last = false
48 let open = false
49 let params = []
50 let brackets = []
51
52 while (!this.tokenizer.endOfFile()) {
53 token = this.tokenizer.nextToken()
54 type = token[0]
55
56 if (type === '(' || type === '[') {
57 brackets.push(type === '(' ? ')' : ']')
58 } else if (type === '{' && brackets.length > 0) {
59 brackets.push('}')
60 } else if (type === brackets[brackets.length - 1]) {
61 brackets.pop()
62 }
63
64 if (brackets.length === 0) {
65 if (type === ';') {
66 node.source.end = this.getPosition(token[2])
67 node.source.end.offset++
68 this.semicolon = true
69 break
70 } else if (type === '{') {
71 open = true
72 break
73 } else if (type === '}') {
74 if (params.length > 0) {
75 shift = params.length - 1
76 prev = params[shift]
77 while (prev && prev[0] === 'space') {
78 prev = params[--shift]
79 }
80 if (prev) {
81 node.source.end = this.getPosition(prev[3] || prev[2])
82 node.source.end.offset++
83 }
84 }
85 this.end(token)
86 break
87 } else {
88 params.push(token)
89 }
90 } else {
91 params.push(token)
92 }
93
94 if (this.tokenizer.endOfFile()) {
95 last = true
96 break
97 }
98 }
99
100 node.raws.between = this.spacesAndCommentsFromEnd(params)
101 if (params.length) {
102 node.raws.afterName = this.spacesAndCommentsFromStart(params)
103 this.raw(node, 'params', params)
104 if (last) {
105 token = params[params.length - 1]
106 node.source.end = this.getPosition(token[3] || token[2])
107 node.source.end.offset++
108 this.spaces = node.raws.between
109 node.raws.between = ''
110 }
111 } else {
112 node.raws.afterName = ''
113 node.params = ''
114 }
115
116 if (open) {
117 node.nodes = []
118 this.current = node
119 }
120 }
121
122 checkMissedSemicolon(tokens) {
123 let colon = this.colon(tokens)
124 if (colon === false) return
125
126 let founded = 0
127 let token
128 for (let j = colon - 1; j >= 0; j--) {
129 token = tokens[j]
130 if (token[0] !== 'space') {
131 founded += 1
132 if (founded === 2) break
133 }
134 }
135 // If the token is a word, e.g. `!important`, `red` or any other valid property's value.
136 // Then we need to return the colon after that word token. [3] is the "end" colon of that word.
137 // And because we need it after that one we do +1 to get the next one.
138 throw this.input.error(
139 'Missed semicolon',
140 token[0] === 'word' ? token[3] + 1 : token[2]
141 )
142 }
143
144 colon(tokens) {
145 let brackets = 0
146 let token, type, prev
147 for (let [i, element] of tokens.entries()) {
148 token = element
149 type = token[0]
150
151 if (type === '(') {
152 brackets += 1
153 }
154 if (type === ')') {
155 brackets -= 1
156 }
157 if (brackets === 0 && type === ':') {
158 if (!prev) {
159 this.doubleColon(token)
160 } else if (prev[0] === 'word' && prev[1] === 'progid') {
161 continue
162 } else {
163 return i
164 }
165 }
166
167 prev = token
168 }
169 return false
170 }
171
172 comment(token) {
173 let node = new Comment()
174 this.init(node, token[2])
175 node.source.end = this.getPosition(token[3] || token[2])
176 node.source.end.offset++
177
178 let text = token[1].slice(2, -2)
179 if (/^\s*$/.test(text)) {
180 node.text = ''
181 node.raws.left = text
182 node.raws.right = ''
183 } else {
184 let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
185 node.text = match[2]
186 node.raws.left = match[1]
187 node.raws.right = match[3]
188 }
189 }
190
191 createTokenizer() {
192 this.tokenizer = tokenizer(this.input)
193 }
194
195 decl(tokens, customProperty) {
196 let node = new Declaration()
197 this.init(node, tokens[0][2])
198
199 let last = tokens[tokens.length - 1]
200 if (last[0] === ';') {
201 this.semicolon = true
202 tokens.pop()
203 }
204
205 node.source.end = this.getPosition(
206 last[3] || last[2] || findLastWithPosition(tokens)
207 )
208 node.source.end.offset++
209
210 while (tokens[0][0] !== 'word') {
211 if (tokens.length === 1) this.unknownWord(tokens)
212 node.raws.before += tokens.shift()[1]
213 }
214 node.source.start = this.getPosition(tokens[0][2])
215
216 node.prop = ''
217 while (tokens.length) {
218 let type = tokens[0][0]
219 if (type === ':' || type === 'space' || type === 'comment') {
220 break
221 }
222 node.prop += tokens.shift()[1]
223 }
224
225 node.raws.between = ''
226
227 let token
228 while (tokens.length) {
229 token = tokens.shift()
230
231 if (token[0] === ':') {
232 node.raws.between += token[1]
233 break
234 } else {
235 if (token[0] === 'word' && /\w/.test(token[1])) {
236 this.unknownWord([token])
237 }
238 node.raws.between += token[1]
239 }
240 }
241
242 if (node.prop[0] === '_' || node.prop[0] === '*') {
243 node.raws.before += node.prop[0]
244 node.prop = node.prop.slice(1)
245 }
246
247 let firstSpaces = []
248 let next
249 while (tokens.length) {
250 next = tokens[0][0]
251 if (next !== 'space' && next !== 'comment') break
252 firstSpaces.push(tokens.shift())
253 }
254
255 this.precheckMissedSemicolon(tokens)
256
257 for (let i = tokens.length - 1; i >= 0; i--) {
258 token = tokens[i]
259 if (token[1].toLowerCase() === '!important') {
260 node.important = true
261 let string = this.stringFrom(tokens, i)
262 string = this.spacesFromEnd(tokens) + string
263 if (string !== ' !important') node.raws.important = string
264 break
265 } else if (token[1].toLowerCase() === 'important') {
266 let cache = tokens.slice(0)
267 let str = ''
268 for (let j = i; j > 0; j--) {
269 let type = cache[j][0]
270 if (str.trim().indexOf('!') === 0 && type !== 'space') {
271 break
272 }
273 str = cache.pop()[1] + str
274 }
275 if (str.trim().indexOf('!') === 0) {
276 node.important = true
277 node.raws.important = str
278 tokens = cache
279 }
280 }
281
282 if (token[0] !== 'space' && token[0] !== 'comment') {
283 break
284 }
285 }
286
287 let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')
288
289 if (hasWord) {
290 node.raws.between += firstSpaces.map(i => i[1]).join('')
291 firstSpaces = []
292 }
293 this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)
294
295 if (node.value.includes(':') && !customProperty) {
296 this.checkMissedSemicolon(tokens)
297 }
298 }
299
300 doubleColon(token) {
301 throw this.input.error(
302 'Double colon',
303 { offset: token[2] },
304 { offset: token[2] + token[1].length }
305 )
306 }
307
308 emptyRule(token) {
309 let node = new Rule()
310 this.init(node, token[2])
311 node.selector = ''
312 node.raws.between = ''
313 this.current = node
314 }
315
316 end(token) {
317 if (this.current.nodes && this.current.nodes.length) {
318 this.current.raws.semicolon = this.semicolon
319 }
320 this.semicolon = false
321
322 this.current.raws.after = (this.current.raws.after || '') + this.spaces
323 this.spaces = ''
324
325 if (this.current.parent) {
326 this.current.source.end = this.getPosition(token[2])
327 this.current.source.end.offset++
328 this.current = this.current.parent
329 } else {
330 this.unexpectedClose(token)
331 }
332 }
333
334 endFile() {
335 if (this.current.parent) this.unclosedBlock()
336 if (this.current.nodes && this.current.nodes.length) {
337 this.current.raws.semicolon = this.semicolon
338 }
339 this.current.raws.after = (this.current.raws.after || '') + this.spaces
340 this.root.source.end = this.getPosition(this.tokenizer.position())
341 }
342
343 freeSemicolon(token) {
344 this.spaces += token[1]
345 if (this.current.nodes) {
346 let prev = this.current.nodes[this.current.nodes.length - 1]
347 if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {
348 prev.raws.ownSemicolon = this.spaces
349 this.spaces = ''
350 }
351 }
352 }
353
354 // Helpers
355
356 getPosition(offset) {
357 let pos = this.input.fromOffset(offset)
358 return {
359 column: pos.col,
360 line: pos.line,
361 offset
362 }
363 }
364
365 init(node, offset) {
366 this.current.push(node)
367 node.source = {
368 input: this.input,
369 start: this.getPosition(offset)
370 }
371 node.raws.before = this.spaces
372 this.spaces = ''
373 if (node.type !== 'comment') this.semicolon = false
374 }
375
376 other(start) {
377 let end = false
378 let type = null
379 let colon = false
380 let bracket = null
381 let brackets = []
382 let customProperty = start[1].startsWith('--')
383
384 let tokens = []
385 let token = start
386 while (token) {
387 type = token[0]
388 tokens.push(token)
389
390 if (type === '(' || type === '[') {
391 if (!bracket) bracket = token
392 brackets.push(type === '(' ? ')' : ']')
393 } else if (customProperty && colon && type === '{') {
394 if (!bracket) bracket = token
395 brackets.push('}')
396 } else if (brackets.length === 0) {
397 if (type === ';') {
398 if (colon) {
399 this.decl(tokens, customProperty)
400 return
401 } else {
402 break
403 }
404 } else if (type === '{') {
405 this.rule(tokens)
406 return
407 } else if (type === '}') {
408 this.tokenizer.back(tokens.pop())
409 end = true
410 break
411 } else if (type === ':') {
412 colon = true
413 }
414 } else if (type === brackets[brackets.length - 1]) {
415 brackets.pop()
416 if (brackets.length === 0) bracket = null
417 }
418
419 token = this.tokenizer.nextToken()
420 }
421
422 if (this.tokenizer.endOfFile()) end = true
423 if (brackets.length > 0) this.unclosedBracket(bracket)
424
425 if (end && colon) {
426 if (!customProperty) {
427 while (tokens.length) {
428 token = tokens[tokens.length - 1][0]
429 if (token !== 'space' && token !== 'comment') break
430 this.tokenizer.back(tokens.pop())
431 }
432 }
433 this.decl(tokens, customProperty)
434 } else {
435 this.unknownWord(tokens)
436 }
437 }
438
439 parse() {
440 let token
441 while (!this.tokenizer.endOfFile()) {
442 token = this.tokenizer.nextToken()
443
444 switch (token[0]) {
445 case 'space':
446 this.spaces += token[1]
447 break
448
449 case ';':
450 this.freeSemicolon(token)
451 break
452
453 case '}':
454 this.end(token)
455 break
456
457 case 'comment':
458 this.comment(token)
459 break
460
461 case 'at-word':
462 this.atrule(token)
463 break
464
465 case '{':
466 this.emptyRule(token)
467 break
468
469 default:
470 this.other(token)
471 break
472 }
473 }
474 this.endFile()
475 }
476
477 precheckMissedSemicolon(/* tokens */) {
478 // Hook for Safe Parser
479 }
480
481 raw(node, prop, tokens, customProperty) {
482 let token, type
483 let length = tokens.length
484 let value = ''
485 let clean = true
486 let next, prev
487
488 for (let i = 0; i < length; i += 1) {
489 token = tokens[i]
490 type = token[0]
491 if (type === 'space' && i === length - 1 && !customProperty) {
492 clean = false
493 } else if (type === 'comment') {
494 prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'
495 next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'
496 if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
497 if (value.slice(-1) === ',') {
498 clean = false
499 } else {
500 value += token[1]
501 }
502 } else {
503 clean = false
504 }
505 } else {
506 value += token[1]
507 }
508 }
509 if (!clean) {
510 let raw = tokens.reduce((all, i) => all + i[1], '')
511 node.raws[prop] = { raw, value }
512 }
513 node[prop] = value
514 }
515
516 rule(tokens) {
517 tokens.pop()
518
519 let node = new Rule()
520 this.init(node, tokens[0][2])
521
522 node.raws.between = this.spacesAndCommentsFromEnd(tokens)
523 this.raw(node, 'selector', tokens)
524 this.current = node
525 }
526
527 spacesAndCommentsFromEnd(tokens) {
528 let lastTokenType
529 let spaces = ''
530 while (tokens.length) {
531 lastTokenType = tokens[tokens.length - 1][0]
532 if (lastTokenType !== 'space' && lastTokenType !== 'comment') break
533 spaces = tokens.pop()[1] + spaces
534 }
535 return spaces
536 }
537
538 // Errors
539
540 spacesAndCommentsFromStart(tokens) {
541 let next
542 let spaces = ''
543 while (tokens.length) {
544 next = tokens[0][0]
545 if (next !== 'space' && next !== 'comment') break
546 spaces += tokens.shift()[1]
547 }
548 return spaces
549 }
550
551 spacesFromEnd(tokens) {
552 let lastTokenType
553 let spaces = ''
554 while (tokens.length) {
555 lastTokenType = tokens[tokens.length - 1][0]
556 if (lastTokenType !== 'space') break
557 spaces = tokens.pop()[1] + spaces
558 }
559 return spaces
560 }
561
562 stringFrom(tokens, from) {
563 let result = ''
564 for (let i = from; i < tokens.length; i++) {
565 result += tokens[i][1]
566 }
567 tokens.splice(from, tokens.length - from)
568 return result
569 }
570
571 unclosedBlock() {
572 let pos = this.current.source.start
573 throw this.input.error('Unclosed block', pos.line, pos.column)
574 }
575
576 unclosedBracket(bracket) {
577 throw this.input.error(
578 'Unclosed bracket',
579 { offset: bracket[2] },
580 { offset: bracket[2] + 1 }
581 )
582 }
583
584 unexpectedClose(token) {
585 throw this.input.error(
586 'Unexpected }',
587 { offset: token[2] },
588 { offset: token[2] + 1 }
589 )
590 }
591
592 unknownWord(tokens) {
593 throw this.input.error(
594 'Unknown word',
595 { offset: tokens[0][2] },
596 { offset: tokens[0][2] + tokens[0][1].length }
597 )
598 }
599
600 unnamedAtrule(node, token) {
601 throw this.input.error(
602 'At-rule without name',
603 { offset: token[2] },
604 { offset: token[2] + token[1].length }
605 )
606 }
607}
608
609module.exports = Parser