You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			611 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			611 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | 'use strict' | ||
|  | 
 | ||
|  | let Declaration = require('./declaration') | ||
|  | let tokenizer = require('./tokenize') | ||
|  | let Comment = require('./comment') | ||
|  | let AtRule = require('./at-rule') | ||
|  | let Root = require('./root') | ||
|  | let Rule = require('./rule') | ||
|  | 
 | ||
|  | const SAFE_COMMENT_NEIGHBOR = { | ||
|  |   empty: true, | ||
|  |   space: true | ||
|  | } | ||
|  | 
 | ||
|  | function findLastWithPosition(tokens) { | ||
|  |   for (let i = tokens.length - 1; i >= 0; i--) { | ||
|  |     let token = tokens[i] | ||
|  |     let pos = token[3] || token[2] | ||
|  |     if (pos) return pos | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class Parser { | ||
|  |   constructor(input) { | ||
|  |     this.input = input | ||
|  | 
 | ||
|  |     this.root = new Root() | ||
|  |     this.current = this.root | ||
|  |     this.spaces = '' | ||
|  |     this.semicolon = false | ||
|  |     this.customProperty = false | ||
|  | 
 | ||
|  |     this.createTokenizer() | ||
|  |     this.root.source = { input, start: { column: 1, line: 1, offset: 0 } } | ||
|  |   } | ||
|  | 
 | ||
|  |   atrule(token) { | ||
|  |     let node = new AtRule() | ||
|  |     node.name = token[1].slice(1) | ||
|  |     if (node.name === '') { | ||
|  |       this.unnamedAtrule(node, token) | ||
|  |     } | ||
|  |     this.init(node, token[2]) | ||
|  | 
 | ||
|  |     let type | ||
|  |     let prev | ||
|  |     let shift | ||
|  |     let last = false | ||
|  |     let open = false | ||
|  |     let params = [] | ||
|  |     let brackets = [] | ||
|  | 
 | ||
|  |     while (!this.tokenizer.endOfFile()) { | ||
|  |       token = this.tokenizer.nextToken() | ||
|  |       type = token[0] | ||
|  | 
 | ||
|  |       if (type === '(' || type === '[') { | ||
|  |         brackets.push(type === '(' ? ')' : ']') | ||
|  |       } else if (type === '{' && brackets.length > 0) { | ||
|  |         brackets.push('}') | ||
|  |       } else if (type === brackets[brackets.length - 1]) { | ||
|  |         brackets.pop() | ||
|  |       } | ||
|  | 
 | ||
|  |       if (brackets.length === 0) { | ||
|  |         if (type === ';') { | ||
|  |           node.source.end = this.getPosition(token[2]) | ||
|  |           node.source.end.offset++ | ||
|  |           this.semicolon = true | ||
|  |           break | ||
|  |         } else if (type === '{') { | ||
|  |           open = true | ||
|  |           break | ||
|  |         } else if (type === '}') { | ||
|  |           if (params.length > 0) { | ||
|  |             shift = params.length - 1 | ||
|  |             prev = params[shift] | ||
|  |             while (prev && prev[0] === 'space') { | ||
|  |               prev = params[--shift] | ||
|  |             } | ||
|  |             if (prev) { | ||
|  |               node.source.end = this.getPosition(prev[3] || prev[2]) | ||
|  |               node.source.end.offset++ | ||
|  |             } | ||
|  |           } | ||
|  |           this.end(token) | ||
|  |           break | ||
|  |         } else { | ||
|  |           params.push(token) | ||
|  |         } | ||
|  |       } else { | ||
|  |         params.push(token) | ||
|  |       } | ||
|  | 
 | ||
|  |       if (this.tokenizer.endOfFile()) { | ||
|  |         last = true | ||
|  |         break | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     node.raws.between = this.spacesAndCommentsFromEnd(params) | ||
|  |     if (params.length) { | ||
|  |       node.raws.afterName = this.spacesAndCommentsFromStart(params) | ||
|  |       this.raw(node, 'params', params) | ||
|  |       if (last) { | ||
|  |         token = params[params.length - 1] | ||
|  |         node.source.end = this.getPosition(token[3] || token[2]) | ||
|  |         node.source.end.offset++ | ||
|  |         this.spaces = node.raws.between | ||
|  |         node.raws.between = '' | ||
|  |       } | ||
|  |     } else { | ||
|  |       node.raws.afterName = '' | ||
|  |       node.params = '' | ||
|  |     } | ||
|  | 
 | ||
|  |     if (open) { | ||
|  |       node.nodes = [] | ||
|  |       this.current = node | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   checkMissedSemicolon(tokens) { | ||
|  |     let colon = this.colon(tokens) | ||
|  |     if (colon === false) return | ||
|  | 
 | ||
|  |     let founded = 0 | ||
|  |     let token | ||
|  |     for (let j = colon - 1; j >= 0; j--) { | ||
|  |       token = tokens[j] | ||
|  |       if (token[0] !== 'space') { | ||
|  |         founded += 1 | ||
|  |         if (founded === 2) break | ||
|  |       } | ||
|  |     } | ||
|  |     // If the token is a word, e.g. `!important`, `red` or any other valid property's value.
 | ||
|  |     // Then we need to return the colon after that word token. [3] is the "end" colon of that word.
 | ||
|  |     // And because we need it after that one we do +1 to get the next one.
 | ||
|  |     throw this.input.error( | ||
|  |       'Missed semicolon', | ||
|  |       token[0] === 'word' ? token[3] + 1 : token[2] | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   colon(tokens) { | ||
|  |     let brackets = 0 | ||
|  |     let token, type, prev | ||
|  |     for (let [i, element] of tokens.entries()) { | ||
|  |       token = element | ||
|  |       type = token[0] | ||
|  | 
 | ||
|  |       if (type === '(') { | ||
|  |         brackets += 1 | ||
|  |       } | ||
|  |       if (type === ')') { | ||
|  |         brackets -= 1 | ||
|  |       } | ||
|  |       if (brackets === 0 && type === ':') { | ||
|  |         if (!prev) { | ||
|  |           this.doubleColon(token) | ||
|  |         } else if (prev[0] === 'word' && prev[1] === 'progid') { | ||
|  |           continue | ||
|  |         } else { | ||
|  |           return i | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       prev = token | ||
|  |     } | ||
|  |     return false | ||
|  |   } | ||
|  | 
 | ||
|  |   comment(token) { | ||
|  |     let node = new Comment() | ||
|  |     this.init(node, token[2]) | ||
|  |     node.source.end = this.getPosition(token[3] || token[2]) | ||
|  |     node.source.end.offset++ | ||
|  | 
 | ||
|  |     let text = token[1].slice(2, -2) | ||
|  |     if (/^\s*$/.test(text)) { | ||
|  |       node.text = '' | ||
|  |       node.raws.left = text | ||
|  |       node.raws.right = '' | ||
|  |     } else { | ||
|  |       let match = text.match(/^(\s*)([^]*\S)(\s*)$/) | ||
|  |       node.text = match[2] | ||
|  |       node.raws.left = match[1] | ||
|  |       node.raws.right = match[3] | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   createTokenizer() { | ||
|  |     this.tokenizer = tokenizer(this.input) | ||
|  |   } | ||
|  | 
 | ||
|  |   decl(tokens, customProperty) { | ||
|  |     let node = new Declaration() | ||
|  |     this.init(node, tokens[0][2]) | ||
|  | 
 | ||
|  |     let last = tokens[tokens.length - 1] | ||
|  |     if (last[0] === ';') { | ||
|  |       this.semicolon = true | ||
|  |       tokens.pop() | ||
|  |     } | ||
|  | 
 | ||
|  |     node.source.end = this.getPosition( | ||
|  |       last[3] || last[2] || findLastWithPosition(tokens) | ||
|  |     ) | ||
|  |     node.source.end.offset++ | ||
|  | 
 | ||
|  |     while (tokens[0][0] !== 'word') { | ||
|  |       if (tokens.length === 1) this.unknownWord(tokens) | ||
|  |       node.raws.before += tokens.shift()[1] | ||
|  |     } | ||
|  |     node.source.start = this.getPosition(tokens[0][2]) | ||
|  | 
 | ||
|  |     node.prop = '' | ||
|  |     while (tokens.length) { | ||
|  |       let type = tokens[0][0] | ||
|  |       if (type === ':' || type === 'space' || type === 'comment') { | ||
|  |         break | ||
|  |       } | ||
|  |       node.prop += tokens.shift()[1] | ||
|  |     } | ||
|  | 
 | ||
|  |     node.raws.between = '' | ||
|  | 
 | ||
|  |     let token | ||
|  |     while (tokens.length) { | ||
|  |       token = tokens.shift() | ||
|  | 
 | ||
|  |       if (token[0] === ':') { | ||
|  |         node.raws.between += token[1] | ||
|  |         break | ||
|  |       } else { | ||
|  |         if (token[0] === 'word' && /\w/.test(token[1])) { | ||
|  |           this.unknownWord([token]) | ||
|  |         } | ||
|  |         node.raws.between += token[1] | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (node.prop[0] === '_' || node.prop[0] === '*') { | ||
|  |       node.raws.before += node.prop[0] | ||
|  |       node.prop = node.prop.slice(1) | ||
|  |     } | ||
|  | 
 | ||
|  |     let firstSpaces = [] | ||
|  |     let next | ||
|  |     while (tokens.length) { | ||
|  |       next = tokens[0][0] | ||
|  |       if (next !== 'space' && next !== 'comment') break | ||
|  |       firstSpaces.push(tokens.shift()) | ||
|  |     } | ||
|  | 
 | ||
|  |     this.precheckMissedSemicolon(tokens) | ||
|  | 
 | ||
|  |     for (let i = tokens.length - 1; i >= 0; i--) { | ||
|  |       token = tokens[i] | ||
|  |       if (token[1].toLowerCase() === '!important') { | ||
|  |         node.important = true | ||
|  |         let string = this.stringFrom(tokens, i) | ||
|  |         string = this.spacesFromEnd(tokens) + string | ||
|  |         if (string !== ' !important') node.raws.important = string | ||
|  |         break | ||
|  |       } else if (token[1].toLowerCase() === 'important') { | ||
|  |         let cache = tokens.slice(0) | ||
|  |         let str = '' | ||
|  |         for (let j = i; j > 0; j--) { | ||
|  |           let type = cache[j][0] | ||
|  |           if (str.trim().indexOf('!') === 0 && type !== 'space') { | ||
|  |             break | ||
|  |           } | ||
|  |           str = cache.pop()[1] + str | ||
|  |         } | ||
|  |         if (str.trim().indexOf('!') === 0) { | ||
|  |           node.important = true | ||
|  |           node.raws.important = str | ||
|  |           tokens = cache | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if (token[0] !== 'space' && token[0] !== 'comment') { | ||
|  |         break | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment') | ||
|  | 
 | ||
|  |     if (hasWord) { | ||
|  |       node.raws.between += firstSpaces.map(i => i[1]).join('') | ||
|  |       firstSpaces = [] | ||
|  |     } | ||
|  |     this.raw(node, 'value', firstSpaces.concat(tokens), customProperty) | ||
|  | 
 | ||
|  |     if (node.value.includes(':') && !customProperty) { | ||
|  |       this.checkMissedSemicolon(tokens) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   doubleColon(token) { | ||
|  |     throw this.input.error( | ||
|  |       'Double colon', | ||
|  |       { offset: token[2] }, | ||
|  |       { offset: token[2] + token[1].length } | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   emptyRule(token) { | ||
|  |     let node = new Rule() | ||
|  |     this.init(node, token[2]) | ||
|  |     node.selector = '' | ||
|  |     node.raws.between = '' | ||
|  |     this.current = node | ||
|  |   } | ||
|  | 
 | ||
|  |   end(token) { | ||
|  |     if (this.current.nodes && this.current.nodes.length) { | ||
|  |       this.current.raws.semicolon = this.semicolon | ||
|  |     } | ||
|  |     this.semicolon = false | ||
|  | 
 | ||
|  |     this.current.raws.after = (this.current.raws.after || '') + this.spaces | ||
|  |     this.spaces = '' | ||
|  | 
 | ||
|  |     if (this.current.parent) { | ||
|  |       this.current.source.end = this.getPosition(token[2]) | ||
|  |       this.current.source.end.offset++ | ||
|  |       this.current = this.current.parent | ||
|  |     } else { | ||
|  |       this.unexpectedClose(token) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   endFile() { | ||
|  |     if (this.current.parent) this.unclosedBlock() | ||
|  |     if (this.current.nodes && this.current.nodes.length) { | ||
|  |       this.current.raws.semicolon = this.semicolon | ||
|  |     } | ||
|  |     this.current.raws.after = (this.current.raws.after || '') + this.spaces | ||
|  |     this.root.source.end = this.getPosition(this.tokenizer.position()) | ||
|  |   } | ||
|  | 
 | ||
|  |   freeSemicolon(token) { | ||
|  |     this.spaces += token[1] | ||
|  |     if (this.current.nodes) { | ||
|  |       let prev = this.current.nodes[this.current.nodes.length - 1] | ||
|  |       if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) { | ||
|  |         prev.raws.ownSemicolon = this.spaces | ||
|  |         this.spaces = '' | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Helpers
 | ||
|  | 
 | ||
|  |   getPosition(offset) { | ||
|  |     let pos = this.input.fromOffset(offset) | ||
|  |     return { | ||
|  |       column: pos.col, | ||
|  |       line: pos.line, | ||
|  |       offset | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   init(node, offset) { | ||
|  |     this.current.push(node) | ||
|  |     node.source = { | ||
|  |       input: this.input, | ||
|  |       start: this.getPosition(offset) | ||
|  |     } | ||
|  |     node.raws.before = this.spaces | ||
|  |     this.spaces = '' | ||
|  |     if (node.type !== 'comment') this.semicolon = false | ||
|  |   } | ||
|  | 
 | ||
|  |   other(start) { | ||
|  |     let end = false | ||
|  |     let type = null | ||
|  |     let colon = false | ||
|  |     let bracket = null | ||
|  |     let brackets = [] | ||
|  |     let customProperty = start[1].startsWith('--') | ||
|  | 
 | ||
|  |     let tokens = [] | ||
|  |     let token = start | ||
|  |     while (token) { | ||
|  |       type = token[0] | ||
|  |       tokens.push(token) | ||
|  | 
 | ||
|  |       if (type === '(' || type === '[') { | ||
|  |         if (!bracket) bracket = token | ||
|  |         brackets.push(type === '(' ? ')' : ']') | ||
|  |       } else if (customProperty && colon && type === '{') { | ||
|  |         if (!bracket) bracket = token | ||
|  |         brackets.push('}') | ||
|  |       } else if (brackets.length === 0) { | ||
|  |         if (type === ';') { | ||
|  |           if (colon) { | ||
|  |             this.decl(tokens, customProperty) | ||
|  |             return | ||
|  |           } else { | ||
|  |             break | ||
|  |           } | ||
|  |         } else if (type === '{') { | ||
|  |           this.rule(tokens) | ||
|  |           return | ||
|  |         } else if (type === '}') { | ||
|  |           this.tokenizer.back(tokens.pop()) | ||
|  |           end = true | ||
|  |           break | ||
|  |         } else if (type === ':') { | ||
|  |           colon = true | ||
|  |         } | ||
|  |       } else if (type === brackets[brackets.length - 1]) { | ||
|  |         brackets.pop() | ||
|  |         if (brackets.length === 0) bracket = null | ||
|  |       } | ||
|  | 
 | ||
|  |       token = this.tokenizer.nextToken() | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this.tokenizer.endOfFile()) end = true | ||
|  |     if (brackets.length > 0) this.unclosedBracket(bracket) | ||
|  | 
 | ||
|  |     if (end && colon) { | ||
|  |       if (!customProperty) { | ||
|  |         while (tokens.length) { | ||
|  |           token = tokens[tokens.length - 1][0] | ||
|  |           if (token !== 'space' && token !== 'comment') break | ||
|  |           this.tokenizer.back(tokens.pop()) | ||
|  |         } | ||
|  |       } | ||
|  |       this.decl(tokens, customProperty) | ||
|  |     } else { | ||
|  |       this.unknownWord(tokens) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   parse() { | ||
|  |     let token | ||
|  |     while (!this.tokenizer.endOfFile()) { | ||
|  |       token = this.tokenizer.nextToken() | ||
|  | 
 | ||
|  |       switch (token[0]) { | ||
|  |         case 'space': | ||
|  |           this.spaces += token[1] | ||
|  |           break | ||
|  | 
 | ||
|  |         case ';': | ||
|  |           this.freeSemicolon(token) | ||
|  |           break | ||
|  | 
 | ||
|  |         case '}': | ||
|  |           this.end(token) | ||
|  |           break | ||
|  | 
 | ||
|  |         case 'comment': | ||
|  |           this.comment(token) | ||
|  |           break | ||
|  | 
 | ||
|  |         case 'at-word': | ||
|  |           this.atrule(token) | ||
|  |           break | ||
|  | 
 | ||
|  |         case '{': | ||
|  |           this.emptyRule(token) | ||
|  |           break | ||
|  | 
 | ||
|  |         default: | ||
|  |           this.other(token) | ||
|  |           break | ||
|  |       } | ||
|  |     } | ||
|  |     this.endFile() | ||
|  |   } | ||
|  | 
 | ||
|  |   precheckMissedSemicolon(/* tokens */) { | ||
|  |     // Hook for Safe Parser
 | ||
|  |   } | ||
|  | 
 | ||
|  |   raw(node, prop, tokens, customProperty) { | ||
|  |     let token, type | ||
|  |     let length = tokens.length | ||
|  |     let value = '' | ||
|  |     let clean = true | ||
|  |     let next, prev | ||
|  | 
 | ||
|  |     for (let i = 0; i < length; i += 1) { | ||
|  |       token = tokens[i] | ||
|  |       type = token[0] | ||
|  |       if (type === 'space' && i === length - 1 && !customProperty) { | ||
|  |         clean = false | ||
|  |       } else if (type === 'comment') { | ||
|  |         prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty' | ||
|  |         next = tokens[i + 1] ? tokens[i + 1][0] : 'empty' | ||
|  |         if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) { | ||
|  |           if (value.slice(-1) === ',') { | ||
|  |             clean = false | ||
|  |           } else { | ||
|  |             value += token[1] | ||
|  |           } | ||
|  |         } else { | ||
|  |           clean = false | ||
|  |         } | ||
|  |       } else { | ||
|  |         value += token[1] | ||
|  |       } | ||
|  |     } | ||
|  |     if (!clean) { | ||
|  |       let raw = tokens.reduce((all, i) => all + i[1], '') | ||
|  |       node.raws[prop] = { raw, value } | ||
|  |     } | ||
|  |     node[prop] = value | ||
|  |   } | ||
|  | 
 | ||
|  |   rule(tokens) { | ||
|  |     tokens.pop() | ||
|  | 
 | ||
|  |     let node = new Rule() | ||
|  |     this.init(node, tokens[0][2]) | ||
|  | 
 | ||
|  |     node.raws.between = this.spacesAndCommentsFromEnd(tokens) | ||
|  |     this.raw(node, 'selector', tokens) | ||
|  |     this.current = node | ||
|  |   } | ||
|  | 
 | ||
|  |   spacesAndCommentsFromEnd(tokens) { | ||
|  |     let lastTokenType | ||
|  |     let spaces = '' | ||
|  |     while (tokens.length) { | ||
|  |       lastTokenType = tokens[tokens.length - 1][0] | ||
|  |       if (lastTokenType !== 'space' && lastTokenType !== 'comment') break | ||
|  |       spaces = tokens.pop()[1] + spaces | ||
|  |     } | ||
|  |     return spaces | ||
|  |   } | ||
|  | 
 | ||
|  |   // Errors
 | ||
|  | 
 | ||
|  |   spacesAndCommentsFromStart(tokens) { | ||
|  |     let next | ||
|  |     let spaces = '' | ||
|  |     while (tokens.length) { | ||
|  |       next = tokens[0][0] | ||
|  |       if (next !== 'space' && next !== 'comment') break | ||
|  |       spaces += tokens.shift()[1] | ||
|  |     } | ||
|  |     return spaces | ||
|  |   } | ||
|  | 
 | ||
|  |   spacesFromEnd(tokens) { | ||
|  |     let lastTokenType | ||
|  |     let spaces = '' | ||
|  |     while (tokens.length) { | ||
|  |       lastTokenType = tokens[tokens.length - 1][0] | ||
|  |       if (lastTokenType !== 'space') break | ||
|  |       spaces = tokens.pop()[1] + spaces | ||
|  |     } | ||
|  |     return spaces | ||
|  |   } | ||
|  | 
 | ||
|  |   stringFrom(tokens, from) { | ||
|  |     let result = '' | ||
|  |     for (let i = from; i < tokens.length; i++) { | ||
|  |       result += tokens[i][1] | ||
|  |     } | ||
|  |     tokens.splice(from, tokens.length - from) | ||
|  |     return result | ||
|  |   } | ||
|  | 
 | ||
|  |   unclosedBlock() { | ||
|  |     let pos = this.current.source.start | ||
|  |     throw this.input.error('Unclosed block', pos.line, pos.column) | ||
|  |   } | ||
|  | 
 | ||
|  |   unclosedBracket(bracket) { | ||
|  |     throw this.input.error( | ||
|  |       'Unclosed bracket', | ||
|  |       { offset: bracket[2] }, | ||
|  |       { offset: bracket[2] + 1 } | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   unexpectedClose(token) { | ||
|  |     throw this.input.error( | ||
|  |       'Unexpected }', | ||
|  |       { offset: token[2] }, | ||
|  |       { offset: token[2] + 1 } | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   unknownWord(tokens) { | ||
|  |     throw this.input.error( | ||
|  |       'Unknown word', | ||
|  |       { offset: tokens[0][2] }, | ||
|  |       { offset: tokens[0][2] + tokens[0][1].length } | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   unnamedAtrule(node, token) { | ||
|  |     throw this.input.error( | ||
|  |       'At-rule without name', | ||
|  |       { offset: token[2] }, | ||
|  |       { offset: token[2] + token[1].length } | ||
|  |     ) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = Parser |