| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| export const CSSParserStates = { |
| Initial: 'Initial', |
| Selector: 'Selector', |
| Style: 'Style', |
| PropertyName: 'PropertyName', |
| PropertyValue: 'PropertyValue', |
| AtRule: 'AtRule' |
| }; |
| |
| /** |
| * @param {string} text |
| */ |
| export function parseCSS(text) { |
| _innerParseCSS(text, postMessage); |
| } |
| |
| /** |
| * @param {string} text |
| * @param {function(*)} chunkCallback |
| */ |
| export function _innerParseCSS(text, chunkCallback) { |
| const chunkSize = 100000; // characters per data chunk |
| const lines = text.split('\n'); |
| let rules = []; |
| let processedChunkCharacters = 0; |
| |
| let state = CSSParserStates.Initial; |
| let rule; |
| let property; |
| const UndefTokenType = new Set(); |
| |
| let disabledRules = []; |
| function disabledRulesCallback(chunk) { |
| disabledRules = disabledRules.concat(chunk.chunk); |
| } |
| |
| /** |
| * @param {string} tokenValue |
| * @param {?string} tokenTypes |
| * @param {number} column |
| * @param {number} newColumn |
| */ |
| function processToken(tokenValue, tokenTypes, column, newColumn) { |
| const tokenType = tokenTypes ? new Set(tokenTypes.split(' ')) : UndefTokenType; |
| switch (state) { |
| case CSSParserStates.Initial: |
| if (tokenType.has('qualifier') || tokenType.has('builtin') || tokenType.has('tag')) { |
| rule = { |
| selectorText: tokenValue, |
| lineNumber: lineNumber, |
| columnNumber: column, |
| properties: [], |
| }; |
| state = CSSParserStates.Selector; |
| } else if (tokenType.has('def')) { |
| rule = { |
| atRule: tokenValue, |
| lineNumber: lineNumber, |
| columnNumber: column, |
| }; |
| state = CSSParserStates.AtRule; |
| } |
| break; |
| case CSSParserStates.Selector: |
| if (tokenValue === '{' && tokenType === UndefTokenType) { |
| rule.selectorText = rule.selectorText.trim(); |
| rule.styleRange = createRange(lineNumber, newColumn); |
| state = CSSParserStates.Style; |
| } else { |
| rule.selectorText += tokenValue; |
| } |
| break; |
| case CSSParserStates.AtRule: |
| if ((tokenValue === ';' || tokenValue === '{') && tokenType === UndefTokenType) { |
| rule.atRule = rule.atRule.trim(); |
| rules.push(rule); |
| state = CSSParserStates.Initial; |
| } else { |
| rule.atRule += tokenValue; |
| } |
| break; |
| case CSSParserStates.Style: |
| if (tokenType.has('meta') || tokenType.has('property')) { |
| property = { |
| name: tokenValue, |
| value: '', |
| range: createRange(lineNumber, column), |
| nameRange: createRange(lineNumber, column) |
| }; |
| state = CSSParserStates.PropertyName; |
| } else if (tokenValue === '}' && tokenType === UndefTokenType) { |
| rule.styleRange.endLine = lineNumber; |
| rule.styleRange.endColumn = column; |
| rules.push(rule); |
| state = CSSParserStates.Initial; |
| } else if (tokenType.has('comment')) { |
| // The |processToken| is called per-line, so no token spans more than one line. |
| // Support only a one-line comments. |
| if (tokenValue.substring(0, 2) !== '/*' || tokenValue.substring(tokenValue.length - 2) !== '*/') { |
| break; |
| } |
| const uncommentedText = tokenValue.substring(2, tokenValue.length - 2); |
| const fakeRule = 'a{\n' + uncommentedText + '}'; |
| disabledRules = []; |
| _innerParseCSS(fakeRule, disabledRulesCallback); |
| if (disabledRules.length === 1 && disabledRules[0].properties.length === 1) { |
| const disabledProperty = disabledRules[0].properties[0]; |
| disabledProperty.disabled = true; |
| disabledProperty.range = createRange(lineNumber, column); |
| disabledProperty.range.endColumn = newColumn; |
| const lineOffset = lineNumber - 1; |
| const columnOffset = column + 2; |
| disabledProperty.nameRange.startLine += lineOffset; |
| disabledProperty.nameRange.startColumn += columnOffset; |
| disabledProperty.nameRange.endLine += lineOffset; |
| disabledProperty.nameRange.endColumn += columnOffset; |
| disabledProperty.valueRange.startLine += lineOffset; |
| disabledProperty.valueRange.startColumn += columnOffset; |
| disabledProperty.valueRange.endLine += lineOffset; |
| disabledProperty.valueRange.endColumn += columnOffset; |
| rule.properties.push(disabledProperty); |
| } |
| } |
| break; |
| case CSSParserStates.PropertyName: |
| if (tokenValue === ':' && tokenType === UndefTokenType) { |
| property.name = property.name; |
| property.nameRange.endLine = lineNumber; |
| property.nameRange.endColumn = column; |
| property.valueRange = createRange(lineNumber, newColumn); |
| state = CSSParserStates.PropertyValue; |
| } else if (tokenType.has('property')) { |
| property.name += tokenValue; |
| } |
| break; |
| case CSSParserStates.PropertyValue: |
| if ((tokenValue === ';' || tokenValue === '}') && tokenType === UndefTokenType) { |
| property.value = property.value; |
| property.valueRange.endLine = lineNumber; |
| property.valueRange.endColumn = column; |
| property.range.endLine = lineNumber; |
| property.range.endColumn = tokenValue === ';' ? newColumn : column; |
| rule.properties.push(property); |
| if (tokenValue === '}') { |
| rule.styleRange.endLine = lineNumber; |
| rule.styleRange.endColumn = column; |
| rules.push(rule); |
| state = CSSParserStates.Initial; |
| } else { |
| state = CSSParserStates.Style; |
| } |
| } else if (!tokenType.has('comment')) { |
| property.value += tokenValue; |
| } |
| break; |
| default: |
| console.assert(false, 'Unknown CSS parser state.'); |
| } |
| processedChunkCharacters += newColumn - column; |
| if (processedChunkCharacters > chunkSize) { |
| chunkCallback({chunk: rules, isLastChunk: false}); |
| rules = []; |
| processedChunkCharacters = 0; |
| } |
| } |
| const tokenizer = FormatterWorker.createTokenizer('text/css'); |
| let lineNumber; |
| for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) { |
| const line = lines[lineNumber]; |
| tokenizer(line, processToken); |
| processToken('\n', null, line.length, line.length + 1); |
| } |
| chunkCallback({chunk: rules, isLastChunk: true}); |
| |
| /** |
| * @return {!{startLine: number, startColumn: number, endLine: number, endColumn: number}} |
| */ |
| function createRange(lineNumber, columnNumber) { |
| return {startLine: lineNumber, startColumn: columnNumber, endLine: lineNumber, endColumn: columnNumber}; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.FormatterWorker = self.FormatterWorker || {}; |
| |
| /* Legacy exported object */ |
| FormatterWorker = FormatterWorker || {}; |
| |
| FormatterWorker.CSSParserStates = CSSParserStates; |
| FormatterWorker.parseCSS = parseCSS; |
| FormatterWorker._innerParseCSS = _innerParseCSS; |