UNPKG

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