chrome-devtools-frontend
Version:
Chrome DevTools UI
842 lines (757 loc) • 31.9 kB
text/typescript
// Copyright 2023 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 * as Common from '../../core/common/common.js';
import type * as Platform from '../../core/platform/platform.js';
import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
import type {CSSMatchedStyles, CSSValueSource} from './CSSMatchedStyles.js';
import {
CSSMetadata,
cssMetadata,
type CSSWideKeyword,
CubicBezierKeywordValues,
FontFamilyRegex,
FontPropertiesRegex
} from './CSSMetadata.js';
import type {CSSProperty} from './CSSProperty.js';
import {
ASTUtils,
type BottomUpTreeMatching,
type Match,
matcherBase,
type SyntaxTree,
tokenizeDeclaration,
VariableMatch
} from './CSSPropertyParser.js';
export class AngleMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
computedText(): string {
return this.text;
}
}
// clang-format off
export class AngleMatcher extends matcherBase(AngleMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isAngleAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): AngleMatch|null {
if (node.name !== 'NumberLiteral') {
return null;
}
const unit = node.getChild('Unit');
// TODO(crbug/1138628) handle unitless 0
if (!unit || !['deg', 'grad', 'rad', 'turn'].includes(matching.ast.text(unit))) {
return null;
}
return new AngleMatch(matching.ast.text(node), node);
}
}
function literalToNumber(node: CodeMirror.SyntaxNode, ast: SyntaxTree): number|null {
if (node.type.name !== 'NumberLiteral') {
return null;
}
const text = ast.text(node);
return Number(text.substring(0, text.length - ast.text(node.getChild('Unit')).length));
}
export class ColorMixMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly space: CodeMirror.SyntaxNode[],
readonly color1: CodeMirror.SyntaxNode[], readonly color2: CodeMirror.SyntaxNode[]) {
}
}
// clang-format off
export class ColorMixMatcher extends matcherBase(ColorMixMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isColorAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): ColorMixMatch|null {
if (node.name !== 'CallExpression' || matching.ast.text(node.getChild('Callee')) !== 'color-mix') {
return null;
}
const computedValueTree = tokenizeDeclaration('--property', matching.getComputedText(node));
if (!computedValueTree) {
return null;
}
const value = ASTUtils.declValue(computedValueTree.tree);
if (!value) {
return null;
}
const computedValueArgs = ASTUtils.callArgs(value);
if (computedValueArgs.length !== 3) {
return null;
}
const [space, color1, color2] = computedValueArgs;
// Verify that all arguments are there, and that the space starts with a literal `in`.
if (space.length < 2 || computedValueTree.text(ASTUtils.stripComments(space).next().value) !== 'in' ||
color1.length < 1 || color2.length < 1) {
return null;
}
// Verify there's at most one percentage value for each color.
const p1 =
color1.filter(n => n.type.name === 'NumberLiteral' && computedValueTree.text(n.getChild('Unit')) === '%');
const p2 =
color2.filter(n => n.type.name === 'NumberLiteral' && computedValueTree.text(n.getChild('Unit')) === '%');
if (p1.length > 1 || p2.length > 1) {
return null;
}
// Verify that if both colors carry percentages, they aren't both zero (which is an invalid property value).
if (p1[0] && p2[0] && (literalToNumber(p1[0], computedValueTree) ?? 0) === 0 &&
(literalToNumber(p2[0], computedValueTree) ?? 0) === 0) {
return null;
}
const args = ASTUtils.callArgs(node);
if (args.length !== 3) {
return null;
}
return new ColorMixMatch(matching.ast.text(node), node, args[0], args[1], args[2]);
}
}
// clang-format off
export class URLMatch implements Match {
constructor(
readonly url: Platform.DevToolsPath.UrlString, readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class URLMatcher extends matcherBase(URLMatch) {
// clang-format on
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): URLMatch|null {
if (node.name !== 'CallLiteral') {
return null;
}
const callee = node.getChild('CallTag');
if (!callee || matching.ast.text(callee) !== 'url') {
return null;
}
const [, lparenNode, urlNode, rparenNode] = ASTUtils.siblings(callee);
if (matching.ast.text(lparenNode) !== '(' ||
(urlNode.name !== 'ParenthesizedContent' && urlNode.name !== 'StringLiteral') ||
matching.ast.text(rparenNode) !== ')') {
return null;
}
const text = matching.ast.text(urlNode);
const url = (urlNode.name === 'StringLiteral' ? text.substr(1, text.length - 2) : text.trim()) as
Platform.DevToolsPath.UrlString;
return new URLMatch(url, matching.ast.text(node), node);
}
}
export class LinearGradientMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class LinearGradientMatcher extends matcherBase(LinearGradientMatch) {
// clang-format on
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): Match|null {
const text = matching.ast.text(node);
if (node.name === 'CallExpression' && matching.ast.text(node.getChild('Callee')) === 'linear-gradient') {
return new LinearGradientMatch(text, node);
}
return null;
}
override accepts(propertyName: string): boolean {
return ['background', 'background-image', '-webkit-mask-image'].includes(propertyName);
}
}
export class ColorMatch implements Match {
computedText: (() => string | null)|undefined;
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode,
private readonly currentColorCallback?: () => string | null) {
this.computedText = currentColorCallback;
}
}
// clang-format off
export class ColorMatcher extends matcherBase(ColorMatch) {
constructor(private readonly currentColorCallback?: () => string|null) {
super();
}
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isColorAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): ColorMatch|null {
const text = matching.ast.text(node);
if (node.name === 'ColorLiteral') {
return new ColorMatch(text, node);
}
if (node.name === 'ValueName') {
if (Common.Color.Nicknames.has(text)) {
return new ColorMatch(text, node);
}
if (text.toLowerCase() === 'currentcolor' && this.currentColorCallback) {
const callback = this.currentColorCallback;
return new ColorMatch(text, node, () => callback() ?? text);
}
}
if (node.name === 'CallExpression') {
const callee = node.getChild('Callee');
if (callee && matching.ast.text(callee).match(/^(rgba?|hsla?|hwba?|lab|lch|oklab|oklch|color)$/)) {
return new ColorMatch(text, node);
}
}
return null;
}
}
export class LightDarkColorMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly light: CodeMirror.SyntaxNode[],
readonly dark: CodeMirror.SyntaxNode[]) {
}
}
// clang-format off
export class LightDarkColorMatcher extends matcherBase(LightDarkColorMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isColorAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): LightDarkColorMatch|null {
if (node.name !== 'CallExpression' || matching.ast.text(node.getChild('Callee')) !== 'light-dark') {
return null;
}
const args = ASTUtils.callArgs(node);
if (args.length !== 2 || args[0].length === 0 || args[1].length === 0) {
return null;
}
return new LightDarkColorMatch(matching.ast.text(node), node, args[0], args[1]);
}
}
export class AutoBaseMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly auto: CodeMirror.SyntaxNode[],
readonly base: CodeMirror.SyntaxNode[]) {
}
}
// clang-format off
export class AutoBaseMatcher extends matcherBase(AutoBaseMatch) {
// clang-format on
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): AutoBaseMatch|null {
if (node.name !== 'CallExpression' || matching.ast.text(node.getChild('Callee')) !== '-internal-auto-base') {
return null;
}
const args = ASTUtils.callArgs(node);
if (args.length !== 2 || args[0].length === 0 || args[1].length === 0) {
return null;
}
return new AutoBaseMatch(matching.ast.text(node), node, args[0], args[1]);
}
}
export const enum LinkableNameProperties {
ANIMATION = 'animation',
ANIMATION_NAME = 'animation-name',
FONT_PALETTE = 'font-palette',
POSITION_TRY_FALLBACKS = 'position-try-fallbacks',
POSITION_TRY = 'position-try',
}
const enum AnimationLonghandPart {
DIRECTION = 'direction',
FILL_MODE = 'fill-mode',
PLAY_STATE = 'play-state',
ITERATION_COUNT = 'iteration-count',
EASING_FUNCTION = 'easing-function',
}
export class LinkableNameMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly propertyName: LinkableNameProperties) {
}
}
// clang-format off
export class LinkableNameMatcher extends matcherBase(LinkableNameMatch) {
// clang-format on
private static isLinkableNameProperty(propertyName: string): propertyName is LinkableNameProperties {
const names: string[] = [
LinkableNameProperties.ANIMATION,
LinkableNameProperties.ANIMATION_NAME,
LinkableNameProperties.FONT_PALETTE,
LinkableNameProperties.POSITION_TRY_FALLBACKS,
LinkableNameProperties.POSITION_TRY,
];
return names.includes(propertyName);
}
static readonly identifierAnimationLonghandMap: Map<string, AnimationLonghandPart> = new Map(
Object.entries({
normal: AnimationLonghandPart.DIRECTION,
alternate: AnimationLonghandPart.DIRECTION,
reverse: AnimationLonghandPart.DIRECTION,
'alternate-reverse': AnimationLonghandPart.DIRECTION,
none: AnimationLonghandPart.FILL_MODE,
forwards: AnimationLonghandPart.FILL_MODE,
backwards: AnimationLonghandPart.FILL_MODE,
both: AnimationLonghandPart.FILL_MODE,
running: AnimationLonghandPart.PLAY_STATE,
paused: AnimationLonghandPart.PLAY_STATE,
infinite: AnimationLonghandPart.ITERATION_COUNT,
linear: AnimationLonghandPart.EASING_FUNCTION,
ease: AnimationLonghandPart.EASING_FUNCTION,
'ease-in': AnimationLonghandPart.EASING_FUNCTION,
'ease-out': AnimationLonghandPart.EASING_FUNCTION,
'ease-in-out': AnimationLonghandPart.EASING_FUNCTION,
steps: AnimationLonghandPart.EASING_FUNCTION,
'step-start': AnimationLonghandPart.EASING_FUNCTION,
'step-end': AnimationLonghandPart.EASING_FUNCTION,
}),
);
private matchAnimationNameInShorthand(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): LinkableNameMatch|
null {
// Order is important within each animation definition for distinguishing <keyframes-name> values from other keywords.
// When parsing, keywords that are valid for properties other than animation-name
// whose values were not found earlier in the shorthand must be accepted for those properties rather than for animation-name.
// See the details in: https://w3c.github.io/csswg-drafts/css-animations/#animation.
const text = matching.ast.text(node);
// This is not a known identifier, so return it as `animation-name`.
if (!LinkableNameMatcher.identifierAnimationLonghandMap.has(text)) {
return new LinkableNameMatch(text, node, LinkableNameProperties.ANIMATION);
}
// There can be multiple `animation` declarations splitted by a comma.
// So, we find the declaration nodes that are related to the node argument.
const declarations = ASTUtils.split(ASTUtils.siblings(ASTUtils.declValue(matching.ast.tree)));
const currentDeclarationNodes = declarations.find(
declaration => declaration[0].from <= node.from && declaration[declaration.length - 1].to >= node.to);
if (!currentDeclarationNodes) {
return null;
}
// We reparse here until the node argument since a variable might be
// providing a meaningful value such as a timing keyword,
// that might change the meaning of the node.
const computedText = matching.getComputedTextRange(currentDeclarationNodes[0], node);
const tokenized = tokenizeDeclaration('--p', computedText);
if (!tokenized) {
return null;
}
const identifierCategory =
LinkableNameMatcher.identifierAnimationLonghandMap.get(text); // The category of the node argument
for (let itNode: typeof tokenized.tree|null = ASTUtils.declValue(tokenized.tree); itNode?.nextSibling;
itNode = itNode.nextSibling) {
// Run through all the nodes that come before node argument
// and check whether a value in the same category is found.
// if so, it means our identifier is an `animation-name` keyword.
if (itNode.name === 'ValueName') {
const categoryValue = LinkableNameMatcher.identifierAnimationLonghandMap.get(tokenized.text(itNode));
if (categoryValue && categoryValue === identifierCategory) {
return new LinkableNameMatch(text, node, LinkableNameProperties.ANIMATION);
}
}
}
return null;
}
override accepts(propertyName: string): boolean {
return LinkableNameMatcher.isLinkableNameProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): LinkableNameMatch|null {
const {propertyName} = matching.ast;
const text = matching.ast.text(node);
const parentNode = node.parent;
if (!parentNode) {
return null;
}
const isParentADeclaration = parentNode.name === 'Declaration';
const isInsideVarCall = parentNode.name === 'ArgList' && parentNode.prevSibling?.name === 'Callee' &&
matching.ast.text(parentNode.prevSibling) === 'var';
const isAParentDeclarationOrVarCall = isParentADeclaration || isInsideVarCall;
// `position-try-fallbacks` and `position-try` only accept names with dashed ident.
const shouldMatchOnlyVariableName = propertyName === LinkableNameProperties.POSITION_TRY ||
propertyName === LinkableNameProperties.POSITION_TRY_FALLBACKS;
// We only mark top level nodes or nodes that are inside `var()` expressions as linkable names.
if (!propertyName || (node.name !== 'ValueName' && node.name !== 'VariableName') ||
!isAParentDeclarationOrVarCall || (node.name === 'ValueName' && shouldMatchOnlyVariableName)) {
return null;
}
if (propertyName === 'animation') {
return this.matchAnimationNameInShorthand(node, matching);
}
// The assertion here is safe since this matcher only runs for
// properties with names inside `LinkableNameProperties` (See the `accepts` function.)
return new LinkableNameMatch(text, node, propertyName as LinkableNameProperties);
}
}
export class BezierMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class BezierMatcher extends matcherBase(BezierMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isBezierAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): Match|null {
const text = matching.ast.text(node);
const isCubicBezierKeyword = node.name === 'ValueName' && CubicBezierKeywordValues.has(text);
const isCubicBezierOrLinearFunction = node.name === 'CallExpression' &&
['cubic-bezier', 'linear'].includes(matching.ast.text(node.getChild('Callee')));
if (!isCubicBezierKeyword && !isCubicBezierOrLinearFunction) {
return null;
}
return new BezierMatch(text, node);
}
}
export class StringMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class StringMatcher extends matcherBase(StringMatch) {
// clang-format on
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): Match|null {
return node.name === 'StringLiteral' ? new StringMatch(matching.ast.text(node), node) : null;
}
}
export const enum ShadowType {
BOX_SHADOW = 'boxShadow',
TEXT_SHADOW = 'textShadow',
}
export class ShadowMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly shadowType: ShadowType) {
}
}
// clang-format off
export class ShadowMatcher extends matcherBase(ShadowMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isShadowProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): ShadowMatch|null {
if (node.name !== 'Declaration') {
return null;
}
const valueNodes = ASTUtils.siblings(ASTUtils.declValue(node));
if (valueNodes.length === 0) {
return null;
}
const valueText = matching.ast.textRange(valueNodes[0], valueNodes[valueNodes.length - 1]);
return new ShadowMatch(
valueText, node, matching.ast.propertyName === 'text-shadow' ? ShadowType.TEXT_SHADOW : ShadowType.BOX_SHADOW);
}
}
export class FontMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class FontMatcher extends matcherBase(FontMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isFontAwareProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): Match|null {
if (node.name !== 'Declaration') {
return null;
}
const regex = matching.ast.propertyName === 'font-family' ? FontFamilyRegex : FontPropertiesRegex;
const valueNodes = ASTUtils.siblings(ASTUtils.declValue(node));
if (valueNodes.length === 0) {
return null;
}
const valueText = matching.ast.textRange(valueNodes[0], valueNodes[valueNodes.length - 1]);
return regex.test(valueText) ? new FontMatch(valueText, node) : null;
}
}
export class LengthMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly unit: string) {
}
}
// clang-format off
export class LengthMatcher extends matcherBase(LengthMatch) {
// clang-format on
static readonly LENGTH_UNITS = new Set([
'em', 'ex', 'ch', 'cap', 'ic', 'lh', 'rem', 'rex', 'rch', 'rlh', 'ric', 'rcap', 'px', 'pt',
'pc', 'in', 'cm', 'mm', 'Q', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 'dvw', 'dvh', 'dvi',
'dvb', 'dvmin', 'dvmax', 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin',
'lvmax', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cqem', 'cqlh', 'cqex', 'cqch',
]);
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): LengthMatch|null {
if (node.name !== 'NumberLiteral') {
return null;
}
const unit = matching.ast.text(node.getChild('Unit'));
if (!LengthMatcher.LENGTH_UNITS.has(unit)) {
return null;
}
const text = matching.ast.text(node);
return new LengthMatch(text, node, unit);
}
}
export class SelectFunctionMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly func: string,
readonly args: CodeMirror.SyntaxNode[][]) {
}
}
// clang-format off
export class SelectFunctionMatcher extends matcherBase(SelectFunctionMatch) {
// clang-format on
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): SelectFunctionMatch|null {
if (node.name !== 'CallExpression') {
return null;
}
const callee = matching.ast.text(node.getChild('Callee'));
if (!['min', 'max', 'clamp'].includes(callee)) {
return null;
}
const args = ASTUtils.callArgs(node);
if (args.some(arg => arg.length === 0 || matching.hasUnresolvedVarsRange(arg[0], arg[arg.length - 1]))) {
return null;
}
const text = matching.ast.text(node);
return new SelectFunctionMatch(text, node, callee, args);
}
}
export class FlexGridMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly isFlex: boolean) {
}
}
// clang-format off
export class FlexGridMatcher extends matcherBase(FlexGridMatch) {
// clang-format on
static readonly FLEX = ['flex', 'inline-flex', 'block flex', 'inline flex'];
static readonly GRID = ['grid', 'inline-grid', 'block grid', 'inline grid'];
override accepts(propertyName: string): boolean {
return propertyName === 'display';
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): FlexGridMatch|null {
if (node.name !== 'Declaration') {
return null;
}
const valueNodes = ASTUtils.siblings(ASTUtils.declValue(node));
if (valueNodes.length < 1) {
return null;
}
const values = valueNodes.filter(node => node.name !== 'Important')
.map(node => matching.getComputedText(node).trim())
.filter(value => value);
const text = values.join(' ');
if (FlexGridMatcher.FLEX.includes(text)) {
return new FlexGridMatch(matching.ast.text(node), node, true);
}
if (FlexGridMatcher.GRID.includes(text)) {
return new FlexGridMatch(matching.ast.text(node), node, false);
}
return null;
}
}
export class GridTemplateMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly lines: CodeMirror.SyntaxNode[][]) {
}
}
// clang-format off
export class GridTemplateMatcher extends matcherBase(GridTemplateMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return cssMetadata().isGridAreaDefiningProperty(propertyName);
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): GridTemplateMatch|null {
if (node.name !== 'Declaration' || matching.hasUnresolvedVars(node)) {
return null;
}
const lines: CodeMirror.SyntaxNode[][] = [];
let curLine: CodeMirror.SyntaxNode[] = [];
// The following two states are designed to consume different cases of LineNames:
// 1. no LineNames in between StringLiterals;
// 2. one LineNames in between, which means the LineNames belongs to the current line;
// 3. two LineNames in between, which means the second LineNames starts a new line.
// `hasLeadingLineNames` tracks if the current row already starts with a LineNames and
// with no following StringLiteral yet, which means that the next StringLiteral should
// be appended to the same `curLine`, instead of creating a new line.
let hasLeadingLineNames = false;
// `needClosingLineNames` tracks if the current row can still consume an optional LineNames,
// which will decide if we should start a new line or not when a LineNames is encountered.
let needClosingLineNames = false;
// Gather row definitions of [<line-names>? <string> <track-size>? <line-names>?], which
// be rendered into separate lines.
function parseNodes(nodes: CodeMirror.SyntaxNode[], varParsingMode = false): void {
for (const curNode of nodes) {
if (matching.getMatch(curNode) instanceof VariableMatch) {
const computedValueTree = tokenizeDeclaration('--property', matching.getComputedText(curNode));
if (!computedValueTree) {
continue;
}
const varNodes = ASTUtils.siblings(ASTUtils.declValue(computedValueTree.tree));
if (varNodes.length === 0) {
continue;
}
if ((varNodes[0].name === 'StringLiteral' && !hasLeadingLineNames) ||
(varNodes[0].name === 'LineNames' && !needClosingLineNames)) {
// The variable value either starts with a string, or with a line name that belongs to a new row;
// therefore we start a new line with the variable.
lines.push(curLine);
curLine = [curNode];
} else {
curLine.push(curNode);
}
// We parse computed nodes of this variable to correctly advance local states, but
// these computed nodes won't be added to the lines.
parseNodes(varNodes, true);
} else if (curNode.name === 'BinaryExpression') {
parseNodes(ASTUtils.siblings(curNode.firstChild));
} else if (curNode.name === 'StringLiteral') {
if (!varParsingMode) {
if (hasLeadingLineNames) {
curLine.push(curNode);
} else {
lines.push(curLine);
curLine = [curNode];
}
}
needClosingLineNames = true;
hasLeadingLineNames = false;
} else if (curNode.name === 'LineNames') {
if (!varParsingMode) {
if (needClosingLineNames) {
curLine.push(curNode);
} else {
lines.push(curLine);
curLine = [curNode];
}
}
hasLeadingLineNames = !needClosingLineNames;
needClosingLineNames = !needClosingLineNames;
} else if (!varParsingMode) {
curLine.push(curNode);
}
}
}
const valueNodes = ASTUtils.siblings(ASTUtils.declValue(node));
parseNodes(valueNodes);
lines.push(curLine);
const valueText = matching.ast.textRange(valueNodes[0], valueNodes[valueNodes.length - 1]);
return new GridTemplateMatch(valueText, node, lines.filter(line => line.length > 0));
}
}
export class AnchorFunctionMatch implements Match {
constructor(readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly functionName: string|null) {
}
}
// clang-format off
export class AnchorFunctionMatcher extends matcherBase(AnchorFunctionMatch) {
// clang-format on
anchorFunction(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): string|null {
if (node.name !== 'CallExpression') {
return null;
}
const calleeText = matching.ast.text(node.getChild('Callee'));
if (calleeText === 'anchor' || calleeText === 'anchor-size') {
return calleeText;
}
return null;
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): AnchorFunctionMatch|null {
if (node.name === 'VariableName') {
// Double-dashed anchor reference to be rendered with a link to its matching anchor.
let parent = node.parent;
if (!parent || parent.name !== 'ArgList') {
return null;
}
parent = parent.parent;
if (!parent || !this.anchorFunction(parent, matching)) {
return null;
}
return new AnchorFunctionMatch(matching.ast.text(node), node, null);
}
const calleeText = this.anchorFunction(node, matching);
if (!calleeText) {
return null;
}
// Match if the anchor/anchor-size function implicitly references an anchor.
const args = ASTUtils.children(node.getChild('ArgList'));
if (calleeText === 'anchor' && args.length <= 2) {
return null;
}
if (args.find(arg => arg.name === 'VariableName')) {
// We have an explicit anchor reference, no need to render swatch.
return null;
}
return new AnchorFunctionMatch(matching.ast.text(node), node, calleeText);
}
}
// For linking `position-anchor: --anchor-name`.
export class PositionAnchorMatch implements Match {
constructor(readonly text: string, readonly matching: BottomUpTreeMatching, readonly node: CodeMirror.SyntaxNode) {
}
}
// clang-format off
export class PositionAnchorMatcher extends matcherBase(PositionAnchorMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return propertyName === 'position-anchor';
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): PositionAnchorMatch|null {
if (node.name !== 'VariableName') {
return null;
}
const dashedIdentifier = matching.ast.text(node);
return new PositionAnchorMatch(dashedIdentifier, matching, node);
}
}
export class CSSWideKeywordMatch implements Match {
constructor(
readonly text: CSSWideKeyword, readonly node: CodeMirror.SyntaxNode, readonly property: CSSProperty,
readonly matchedStyles: CSSMatchedStyles) {
}
resolveProperty(): CSSValueSource|null {
return this.matchedStyles.resolveGlobalKeyword(this.property, this.text);
}
computedText?(): string|null {
return this.resolveProperty()?.value ?? null;
}
}
// clang-format off
export class CSSWideKeywordMatcher extends matcherBase(CSSWideKeywordMatch) {
// clang-format on
constructor(readonly property: CSSProperty, readonly matchedStyles: CSSMatchedStyles) {
super();
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): CSSWideKeywordMatch|null {
const parentNode = node.parent;
if (node.name !== 'ValueName' || parentNode?.name !== 'Declaration') {
return null;
}
if (Array.from(ASTUtils.stripComments(ASTUtils.siblings(ASTUtils.declValue(parentNode))))
.some(child => !ASTUtils.equals(child, node))) {
return null;
}
const text = matching.ast.text(node);
if (!CSSMetadata.isCSSWideKeyword(text)) {
return null;
}
return new CSSWideKeywordMatch(text, node, this.property, this.matchedStyles);
}
}
export class PositionTryMatch implements Match {
constructor(
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly preamble: CodeMirror.SyntaxNode[],
readonly fallbacks: CodeMirror.SyntaxNode[][]) {
}
}
// clang-format off
export class PositionTryMatcher extends matcherBase(PositionTryMatch) {
// clang-format on
override accepts(propertyName: string): boolean {
return propertyName === LinkableNameProperties.POSITION_TRY ||
propertyName === LinkableNameProperties.POSITION_TRY_FALLBACKS;
}
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): PositionTryMatch|null {
if (node.name !== 'Declaration') {
return null;
}
let preamble: CodeMirror.SyntaxNode[] = [];
const valueNodes = ASTUtils.siblings(ASTUtils.declValue(node));
const fallbacks = ASTUtils.split(valueNodes);
if (matching.ast.propertyName === LinkableNameProperties.POSITION_TRY) {
for (const [i, n] of fallbacks[0].entries()) {
const computedText = matching.getComputedText(n);
if (CSSMetadata.isCSSWideKeyword(computedText)) {
return null;
}
if (CSSMetadata.isPositionTryOrderKeyword(computedText)) {
preamble = fallbacks[0].splice(0, i + 1);
break;
}
}
}
const valueText = matching.ast.textRange(valueNodes[0], valueNodes[valueNodes.length - 1]);
return new PositionTryMatch(valueText, node, preamble, fallbacks);
}
}