blob: b5cc522f32fd9894f7aedb7aba0bb3b1575d8c5f [file] [log] [blame]
// 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;