UNPKG

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