Skip to content

Commit 86fb208

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[styles] Support evaluating env()
Fixed: 40196710 Change-Id: I6b7bc865d13841a90e0451ffcd26fb6866ba2f22 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6760832 Auto-Submit: Philip Pfaffe <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]> Reviewed-by: Changhao Han <[email protected]>
1 parent 64db15e commit 86fb208

12 files changed

+269
-99
lines changed

front_end/core/sdk/CSSMatchedStyles.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as Protocol from '../../generated/protocol.js';
66
import {createTarget} from '../../testing/EnvironmentHelpers.js';
7-
import {describeWithMockConnection} from '../../testing/MockConnection.js';
7+
import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js';
88
import {getMatchedStyles, ruleMatch} from '../../testing/StyleHelpers.js';
99

1010
import * as SDK from './sdk.js';
@@ -700,6 +700,7 @@ describe('CSSMatchedStyles', () => {
700700

701701
describeWithMockConnection('NodeCascade', () => {
702702
it('correctly marks custom properties as Overloaded if they are registered as inherits: false', async () => {
703+
setMockConnectionResponseHandler('CSS.getEnvironmentVariables', () => ({}));
703704
const target = createTarget();
704705
const cssModel = new SDK.CSSModel.CSSModel(target);
705706
const parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);

front_end/core/sdk/CSSMatchedStyles.ts

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
BinOpMatcher,
2020
ColorMatcher,
2121
ColorMixMatcher,
22+
EnvFunctionMatcher,
2223
FlexGridMatcher,
2324
GridTemplateMatcher,
2425
LengthMatcher,
@@ -296,6 +297,7 @@ export class CSSMatchedStyles {
296297
#functionRules: CSSFunctionRule[];
297298
#functionRuleMap = new Map<string, CSSFunctionRule>();
298299
readonly #fontPaletteValuesRule: CSSFontPaletteValuesRule|undefined;
300+
#environmentVariables: Record<string, string> = {};
299301

300302
static async create(payload: CSSMatchedStylesPayload): Promise<CSSMatchedStyles> {
301303
const cssMatchedStyles = new CSSMatchedStyles(payload);
@@ -349,6 +351,8 @@ export class CSSMatchedStyles {
349351
inheritedResult.matchedCSSRules = cleanUserAgentPayload(inheritedResult.matchedCSSRules);
350352
}
351353

354+
this.#environmentVariables = await this.cssModel().getEnvironmentVariales();
355+
352356
this.#mainDOMCascade = await this.buildMainCascade(
353357
inlinePayload, attributesPayload, matchedPayload, inheritedPayload, animationStylesPayload,
354358
transitionsStylePayload, inheritedAnimatedPayload);
@@ -504,7 +508,7 @@ export class CSSMatchedStyles {
504508
nodeCascades.push(new NodeCascade(this, inheritedStyles, true /* #isInherited */));
505509
}
506510

507-
return new DOMInheritanceCascade(nodeCascades, this.#registeredProperties);
511+
return new DOMInheritanceCascade(this, nodeCascades, this.#registeredProperties);
508512
}
509513

510514
/**
@@ -632,12 +636,13 @@ export class CSSMatchedStyles {
632636
// Now that we've built the arrays of NodeCascades for each pseudo type, convert them into
633637
// DOMInheritanceCascades.
634638
for (const [pseudoType, nodeCascade] of pseudoCascades.entries()) {
635-
pseudoInheritanceCascades.set(pseudoType, new DOMInheritanceCascade(nodeCascade, this.#registeredProperties));
639+
pseudoInheritanceCascades.set(
640+
pseudoType, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties));
636641
}
637642

638643
for (const [highlightName, nodeCascade] of customHighlightPseudoCascades.entries()) {
639644
customHighlightPseudoInheritanceCascades.set(
640-
highlightName, new DOMInheritanceCascade(nodeCascade, this.#registeredProperties));
645+
highlightName, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties));
641646
}
642647

643648
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
@@ -904,8 +909,13 @@ export class CSSMatchedStyles {
904909
new AutoBaseMatcher(),
905910
new BinOpMatcher(),
906911
new RelativeColorChannelMatcher(),
912+
new EnvFunctionMatcher(this),
907913
];
908914
}
915+
916+
environmentVariable(name: string): string|undefined {
917+
return this.#environmentVariables[name];
918+
}
909919
}
910920

911921
class NodeCascade {
@@ -1085,8 +1095,11 @@ class DOMInheritanceCascade {
10851095
#initialized = false;
10861096
readonly #nodeCascades: NodeCascade[];
10871097
#registeredProperties: CSSRegisteredProperty[];
1088-
constructor(nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[]) {
1098+
readonly #matchedStyles: CSSMatchedStyles;
1099+
constructor(
1100+
matchedStyles: CSSMatchedStyles, nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[]) {
10891101
this.#nodeCascades = nodeCascades;
1102+
this.#matchedStyles = matchedStyles;
10901103
this.#registeredProperties = registeredProperties;
10911104

10921105
for (const nodeCascade of nodeCascades) {
@@ -1267,47 +1280,49 @@ class DOMInheritanceCascade {
12671280
// bubbling up the minimum discovery time whenever we close a cycle.
12681281
const record = sccRecord.add(nodeCascade, variableName);
12691282

1270-
const matching = PropertyParser.BottomUpTreeMatching.walk(
1271-
ast, [new BaseVariableMatcher(match => {
1272-
const parentStyle = definedValue.declaration.style;
1273-
const nodeCascade = this.#styleToNodeCascade.get(parentStyle);
1274-
if (!nodeCascade) {
1283+
const matching = PropertyParser.BottomUpTreeMatching.walk(ast, [
1284+
new BaseVariableMatcher(match => {
1285+
const parentStyle = definedValue.declaration.style;
1286+
const nodeCascade = this.#styleToNodeCascade.get(parentStyle);
1287+
if (!nodeCascade) {
1288+
return null;
1289+
}
1290+
const childRecord = sccRecord.get(nodeCascade, match.name);
1291+
if (childRecord) {
1292+
if (sccRecord.isInInProgressSCC(childRecord)) {
1293+
// Cycle detected, update the root.
1294+
record.updateRoot(childRecord);
12751295
return null;
12761296
}
1277-
const childRecord = sccRecord.get(nodeCascade, match.name);
1278-
if (childRecord) {
1279-
if (sccRecord.isInInProgressSCC(childRecord)) {
1280-
// Cycle detected, update the root.
1281-
record.updateRoot(childRecord);
1282-
return null;
1283-
}
12841297

1285-
// We've seen the variable before, so we can look up the text directly.
1286-
return this.#computedCSSVariables.get(nodeCascade)?.get(match.name)?.value ?? null;
1287-
}
1298+
// We've seen the variable before, so we can look up the text directly.
1299+
return this.#computedCSSVariables.get(nodeCascade)?.get(match.name)?.value ?? null;
1300+
}
12881301

1289-
const cssVariableValue = this.innerComputeCSSVariable(nodeCascade, match.name, sccRecord);
1290-
// Variable reference is resolved, so return it.
1291-
const newChildRecord = sccRecord.get(nodeCascade, match.name);
1292-
// The SCC record for the referenced variable may not exist if the var was already computed in a previous
1293-
// iteration. That means it's in a different SCC.
1294-
newChildRecord && record.updateRoot(newChildRecord);
1295-
if (cssVariableValue?.value !== undefined) {
1296-
return cssVariableValue.value;
1297-
}
1302+
const cssVariableValue = this.innerComputeCSSVariable(nodeCascade, match.name, sccRecord);
1303+
// Variable reference is resolved, so return it.
1304+
const newChildRecord = sccRecord.get(nodeCascade, match.name);
1305+
// The SCC record for the referenced variable may not exist if the var was already computed in a previous
1306+
// iteration. That means it's in a different SCC.
1307+
newChildRecord && record.updateRoot(newChildRecord);
1308+
if (cssVariableValue?.value !== undefined) {
1309+
return cssVariableValue.value;
1310+
}
12981311

1299-
// Variable reference is not resolved, use the fallback.
1300-
if (!match.fallback) {
1301-
return null;
1302-
}
1303-
if (match.fallback.length === 0) {
1304-
return '';
1305-
}
1306-
if (match.matching.hasUnresolvedVarsRange(match.fallback[0], match.fallback[match.fallback.length - 1])) {
1307-
return null;
1308-
}
1309-
return match.matching.getComputedTextRange(match.fallback[0], match.fallback[match.fallback.length - 1]);
1310-
})]);
1312+
// Variable reference is not resolved, use the fallback.
1313+
if (!match.fallback) {
1314+
return null;
1315+
}
1316+
if (match.fallback.length === 0) {
1317+
return '';
1318+
}
1319+
if (match.matching.hasUnresolvedVarsRange(match.fallback[0], match.fallback[match.fallback.length - 1])) {
1320+
return null;
1321+
}
1322+
return match.matching.getComputedTextRange(match.fallback[0], match.fallback[match.fallback.length - 1]);
1323+
}),
1324+
new EnvFunctionMatcher(this.#matchedStyles)
1325+
]);
13111326

13121327
const decl = PropertyParser.ASTUtils.siblings(PropertyParser.ASTUtils.declValue(matching.ast.tree));
13131328
const computedText = decl.length > 0 ? matching.getComputedTextRange(decl[0], decl[decl.length - 1]) : '';

front_end/core/sdk/CSSModel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,14 @@ export class CSSModel extends SDKModel<EventTypes> {
432432
};
433433
}
434434

435+
async getEnvironmentVariales(): Promise<Record<string, string>> {
436+
const response = await this.agent.invoke_getEnvironmentVariables();
437+
if (response.getError()) {
438+
return {};
439+
}
440+
return response.environmentVariables;
441+
}
442+
435443
async getBackgroundColors(nodeId: Protocol.DOM.NodeId): Promise<ContrastInfo|null> {
436444
const response = await this.agent.invoke_getBackgroundColors({nodeId});
437445
if (response.getError()) {

front_end/core/sdk/CSSPropertyParserMatchers.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,4 +732,35 @@ describe('Matchers for SDK.CSSPropertyParser.BottomUpTreeMatching', () => {
732732
new SDK.CSSPropertyParserMatchers.ColorMatch(expected.getAuthoredText() ?? expected.asString(), match.node);
733733
assert.isNull(match.getColorChannelValue({baseColor, colorSpace: expected.format()}));
734734
});
735+
736+
it('match env() functions', () => {
737+
// Matched when the var resolves
738+
for (const good of ['env(a)', 'env(a, d)', 'env(a /* aa */, b c)', 'env(a, b, c)']) {
739+
const matchedStyles = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles);
740+
matchedStyles.environmentVariable.callsFake(name => name === 'a' ? 'A' : 'B');
741+
const {match, text} =
742+
matchSingleValue('--env', good, new SDK.CSSPropertyParserMatchers.EnvFunctionMatcher(matchedStyles));
743+
assert.exists(match, text);
744+
assert.strictEqual(match.varName, 'a');
745+
assert.strictEqual(match.value, 'A');
746+
}
747+
// Matched when the var is not resolved
748+
for (const good of ['env(a)', 'env(a, d)', 'env(a /* aa */, b c)', 'env(a, b, c)']) {
749+
const matchedStyles = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles);
750+
matchedStyles.environmentVariable.callsFake(name => name === 'a' ? undefined : 'B');
751+
const {match, text} =
752+
matchSingleValue('--env', good, new SDK.CSSPropertyParserMatchers.EnvFunctionMatcher(matchedStyles));
753+
assert.exists(match, text);
754+
assert.strictEqual(match.varName, 'a');
755+
assert.oneOf(match.value, [null, 'd', 'b c', 'b, c']);
756+
}
757+
// Not matched
758+
for (const bad of ['env', 'env()']) {
759+
const matchedStyles = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles);
760+
const {match, ast, text} =
761+
matchSingleValue('--env', bad, new SDK.CSSPropertyParserMatchers.EnvFunctionMatcher(matchedStyles));
762+
assert.notExists(match, text);
763+
assert.exists(ast, text);
764+
}
765+
});
735766
});

front_end/core/sdk/CSSPropertyParserMatchers.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,3 +1193,40 @@ export class PositionTryMatcher extends matcherBase(PositionTryMatch) {
11931193
return new PositionTryMatch(valueText, node, preamble, fallbacks);
11941194
}
11951195
}
1196+
1197+
export class EnvFunctionMatch implements Match {
1198+
constructor(
1199+
readonly text: string, readonly node: CodeMirror.SyntaxNode, readonly varName: string,
1200+
readonly value: string|null, readonly varNameIsValid: boolean) {
1201+
}
1202+
1203+
computedText(): string|null {
1204+
return this.value;
1205+
}
1206+
}
1207+
1208+
// clang-format off
1209+
export class EnvFunctionMatcher extends matcherBase(EnvFunctionMatch) {
1210+
// clang-format on
1211+
constructor(readonly matchedStyles: CSSMatchedStyles) {
1212+
super();
1213+
}
1214+
1215+
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): EnvFunctionMatch|null {
1216+
if (node.name !== 'CallExpression' || matching.ast.text(node.getChild('Callee')) !== 'env') {
1217+
return null;
1218+
}
1219+
1220+
const [valueNodes, ...fallbackNodes] = ASTUtils.callArgs(node);
1221+
if (!valueNodes?.length) {
1222+
return null;
1223+
}
1224+
1225+
const fallbackValue =
1226+
fallbackNodes.length > 0 ? matching.getComputedTextRange(...ASTUtils.range(fallbackNodes.flat())) : undefined;
1227+
const varName = matching.getComputedTextRange(...ASTUtils.range(valueNodes)).trim();
1228+
const value = this.matchedStyles.environmentVariable(varName);
1229+
1230+
return new EnvFunctionMatch(matching.ast.text(node), node, varName, value ?? fallbackValue ?? null, Boolean(value));
1231+
}
1232+
}

front_end/panels/elements/CSSValueTraceView.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import * as Elements from './elements.js';
1616
async function setUpStyles() {
1717
stubNoopSettings();
1818
setMockConnectionResponseHandler('CSS.enable', () => ({}));
19+
setMockConnectionResponseHandler('CSS.getEnvironmentVariables', () => ({}));
1920
const computedStyleModel = new Elements.ComputedStyleModel.ComputedStyleModel();
2021
const cssModel = new SDK.CSSModel.CSSModel(createTarget());
2122
await cssModel.resumeModel();
2223
const domModel = cssModel.domModel();
2324
const node = new SDK.DOMModel.DOMNode(domModel);
2425
node.id = 0 as Protocol.DOM.NodeId;
2526
UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
26-
const matchedStyles = await getMatchedStylesWithBlankRule(cssModel);
27+
const matchedStyles = await getMatchedStylesWithBlankRule({cssModel});
2728
const stylesPane = new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel);
2829

2930
return {matchedStyles, stylesPane};

front_end/panels/elements/PropertyRenderer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export class TracingContext {
183183
this.#highlighting = highlighting;
184184
this.#hasMoreSubstitutions =
185185
matchedResult?.hasMatches(
186-
SDK.CSSPropertyParserMatchers.VariableMatch, SDK.CSSPropertyParserMatchers.BaseVariableMatch) ??
186+
SDK.CSSPropertyParserMatchers.VariableMatch, SDK.CSSPropertyParserMatchers.BaseVariableMatch,
187+
SDK.CSSPropertyParserMatchers.EnvFunctionMatch) ??
187188
false;
188189
this.#propertyName = matchedResult?.ast.propertyName ?? null;
189190
this.#longhandOffset = initialLonghandOffset;

front_end/panels/elements/StylePropertiesSection.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describeWithMockConnection('StylesPropertySection', () => {
2121

2222
it('contains specificity information', async () => {
2323
const specificity = {a: 0, b: 1, c: 0};
24-
const matchedStyles = await getMatchedStylesWithBlankRule(new SDK.CSSModel.CSSModel(createTarget()));
24+
const matchedStyles = await getMatchedStylesWithBlankRule({cssModel: new SDK.CSSModel.CSSModel(createTarget())});
2525
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
2626
new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel), matchedStyles,
2727
matchedStyles.nodeStyles()[0], 0, new Map(), new Map());
@@ -32,7 +32,7 @@ describeWithMockConnection('StylesPropertySection', () => {
3232
});
3333

3434
it('renders selectors correctly', async () => {
35-
const matchedStyles = await getMatchedStylesWithBlankRule(new SDK.CSSModel.CSSModel(createTarget()));
35+
const matchedStyles = await getMatchedStylesWithBlankRule({cssModel: new SDK.CSSModel.CSSModel(createTarget())});
3636
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
3737
new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel), matchedStyles,
3838
matchedStyles.nodeStyles()[0], 0, new Map(), new Map());
@@ -69,7 +69,7 @@ describeWithMockConnection('StylesPropertySection', () => {
6969
matchingSelectors: [0],
7070
}];
7171
const matchedStyles =
72-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, header, {matchedPayload});
72+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...header, matchedPayload});
7373

7474
const rule = matchedStyles.nodeStyles()[0].parentRule;
7575
const linkifier = sinon.createStubInstance(Components.Linkifier.Linkifier);
@@ -108,7 +108,7 @@ describeWithMockConnection('StylesPropertySection', () => {
108108
content: url === header.sourceMapURL ? '{"sources": []}' : '',
109109
}));
110110
const matchedStyles =
111-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, header, {matchedPayload});
111+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...header, matchedPayload});
112112

113113
const styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId);
114114
assert.exists(styleSheetHeader);
@@ -152,7 +152,7 @@ describeWithMockConnection('StylesPropertySection', () => {
152152
matchingSelectors: [0],
153153
}];
154154
const matchedStyles =
155-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {matchedPayload});
155+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...range, matchedPayload});
156156
const declaration = matchedStyles.nodeStyles()[0];
157157
assert.exists(declaration);
158158
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
@@ -175,7 +175,7 @@ describeWithMockConnection('StylesPropertySection', () => {
175175
matchingSelectors: [0],
176176
}];
177177
const matchedStyles =
178-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {matchedPayload});
178+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...range, matchedPayload});
179179
const declaration = matchedStyles.nodeStyles()[0];
180180
assert.exists(declaration);
181181
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
@@ -216,8 +216,8 @@ describeWithMockConnection('StylesPropertySection', () => {
216216
matchingSelectors: [0],
217217
}];
218218

219-
const matchedStyles = await getMatchedStylesWithStylesheet(
220-
cssModel, origin, styleSheetId, {...range}, {propertyRules, matchedPayload});
219+
const matchedStyles =
220+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...range, propertyRules, matchedPayload});
221221

222222
function assertIsPropertyRule(rule: SDK.CSSRule.CSSRule|null): asserts rule is SDK.CSSRule.CSSPropertyRule {
223223
assert.instanceOf(rule, SDK.CSSRule.CSSPropertyRule);
@@ -265,7 +265,7 @@ describeWithMockConnection('StylesPropertySection', () => {
265265
},
266266
};
267267
const matchedStyles =
268-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {fontPaletteValuesRule});
268+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...range, fontPaletteValuesRule});
269269
const declaration = matchedStyles.fontPaletteValuesRule()?.style;
270270
assert.exists(declaration);
271271
const section = new Elements.StylePropertiesSection.FontPaletteValuesRuleSection(
@@ -309,7 +309,7 @@ describeWithMockConnection('StylesPropertySection', () => {
309309
},
310310
];
311311
const matchedStyles =
312-
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {positionTryRules});
312+
await getMatchedStylesWithStylesheet({cssModel, origin, styleSheetId, ...range, positionTryRules});
313313
const declaration1 = matchedStyles.positionTryRules()[0].style;
314314
const declaration2 = matchedStyles.positionTryRules()[1].style;
315315
assert.exists(declaration1);

0 commit comments

Comments
 (0)