chrome-devtools-frontend
Version:
Chrome DevTools UI
198 lines (185 loc) • 7.56 kB
text/typescript
// 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.
import {createTokenizer, type Chunk, type ChunkCallback} from './FormatterWorker.js';
export const CSSParserStates = {
Initial: 'Initial',
Selector: 'Selector',
Style: 'Style',
PropertyName: 'PropertyName',
PropertyValue: 'PropertyValue',
AtRule: 'AtRule',
};
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Rule = any;
interface Property {
name: string;
value: string;
range: Range;
nameRange: Range;
valueRange?: Range;
}
interface Range {
startLine: number;
startColumn: number;
endLine: number;
endColumn: number;
}
export function parseCSS(text: string, chunkCallback: ChunkCallback): void {
const chunkSize = 100000; // characters per data chunk
const lines = text.split('\n');
let rules: Rule[] = [];
let processedChunkCharacters = 0;
let state: string = CSSParserStates.Initial;
let rule: Rule;
let property: Property;
const UndefTokenType = new Set();
let disabledRules: Rule[] = [];
function disabledRulesCallback(chunk: Chunk): void {
disabledRules = disabledRules.concat(chunk.chunk);
}
function cssTrim(tokenValue: string): string {
// https://drafts.csswg.org/css-syntax/#whitespace
const re = /^(?:\r?\n|[\t\f\r ])+|(?:\r?\n|[\t\f\r ])+$/g;
return tokenValue.replace(re, '');
}
function processToken(tokenValue: string, tokenTypes: string|null, column: number, newColumn: number): void {
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 = cssTrim(rule.selectorText);
rule.styleRange = createRange(lineNumber, newColumn);
state = CSSParserStates.Style;
} else {
rule.selectorText += tokenValue;
}
break;
case CSSParserStates.AtRule:
if ((tokenValue === ';' || tokenValue === '{') && tokenType === UndefTokenType) {
rule.atRule = cssTrim(rule.atRule);
rules.push(rule);
state = CSSParserStates.Initial;
} else {
rule.atRule += tokenValue;
}
break;
case CSSParserStates.Style:
if (tokenType.has('meta') || tokenType.has('property') || tokenType.has('variable-2')) {
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 = [];
parseCSS(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;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
property.valueRange.endLine = lineNumber;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
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 = createTokenizer('text/css');
let lineNumber: number;
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});
function createRange(lineNumber: number, columnNumber: number): Range {
return {startLine: lineNumber, startColumn: columnNumber, endLine: lineNumber, endColumn: columnNumber};
}
}