1 | 'use strict'
|
2 |
|
3 | let Declaration = require('./declaration')
|
4 | let tokenizer = require('./tokenize')
|
5 | let Comment = require('./comment')
|
6 | let AtRule = require('./at-rule')
|
7 | let Root = require('./root')
|
8 | let Rule = require('./rule')
|
9 |
|
10 | class 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 |
|
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 |
|
512 |
|
513 | unclosedBracket (bracket) {
|
514 | throw this.input.error('Unclosed bracket', bracket[2])
|
515 | }
|
516 |
|
517 | unknownWord (tokens) {
|
518 | throw this.input.error('Unknown word', tokens[0][2])
|
519 | }
|
520 |
|
521 | unexpectedClose (token) {
|
522 | throw this.input.error('Unexpected }', token[2])
|
523 | }
|
524 |
|
525 | unclosedBlock () {
|
526 | let pos = this.current.source.start
|
527 | throw this.input.error('Unclosed block', pos.line, pos.column)
|
528 | }
|
529 |
|
530 | doubleColon (token) {
|
531 | throw this.input.error('Double colon', token[2])
|
532 | }
|
533 |
|
534 | unnamedAtrule (node, token) {
|
535 | throw this.input.error('At-rule without name', token[2])
|
536 | }
|
537 |
|
538 | precheckMissedSemicolon () {
|
539 |
|
540 | }
|
541 |
|
542 | checkMissedSemicolon (tokens) {
|
543 | let colon = this.colon(tokens)
|
544 | if (colon === false) return
|
545 |
|
546 | let founded = 0
|
547 | let token
|
548 | for (let j = colon - 1; j >= 0; j--) {
|
549 | token = tokens[j]
|
550 | if (token[0] !== 'space') {
|
551 | founded += 1
|
552 | if (founded === 2) break
|
553 | }
|
554 | }
|
555 | throw this.input.error('Missed semicolon', token[2])
|
556 | }
|
557 | }
|
558 |
|
559 | module.exports = Parser
|