UNPKG

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