source: frontend/node_modules/postcss/lib/parser.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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