mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-29 17:35:02 +08:00
Bug: none Change-Id: I055b1743d105e2e3f5b95e150dbf687531317d57 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5838137 Auto-Submit: Paul Irish <paulirish@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
198 lines
7.4 KiB
TypeScript
198 lines
7.4 KiB
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,
|
|
columnNumber: column,
|
|
properties: [],
|
|
};
|
|
state = CSSParserStates.Selector;
|
|
} else if (tokenType.has('def')) {
|
|
rule = {
|
|
atRule: tokenValue,
|
|
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};
|
|
}
|
|
}
|