chrome-devtools-frontend
Version:
Chrome DevTools UI
1,240 lines (1,097 loc) • 53.8 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 * as Protocol from '../../generated/protocol.js';
import * as Platform from '../platform/platform.js';
import {CSSMetadata, cssMetadata, CSSWideKeyword} from './CSSMetadata.js';
import type {CSSModel} from './CSSModel.js';
import {CSSProperty} from './CSSProperty.js';
import * as PropertyParser from './CSSPropertyParser.js';
import {
CSSFontPaletteValuesRule,
CSSKeyframesRule,
CSSPositionTryRule,
CSSPropertyRule,
CSSStyleRule,
} from './CSSRule.js';
import {CSSStyleDeclaration, Type} from './CSSStyleDeclaration.js';
import type {DOMNode} from './DOMModel.js';
function containsStyle(styles: CSSStyleDeclaration[]|Set<CSSStyleDeclaration>, query: CSSStyleDeclaration): boolean {
if (!query.styleSheetId || !query.range) {
return false;
}
for (const style of styles) {
if (query.styleSheetId === style.styleSheetId && style.range && query.range.equal(style.range)) {
return true;
}
}
return false;
}
function containsCustomProperties(style: CSSStyleDeclaration): boolean {
const properties = style.allProperties();
return properties.some(property => cssMetadata().isCustomProperty(property.name));
}
function containsInherited(style: CSSStyleDeclaration): boolean {
const properties = style.allProperties();
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
// Does this style contain non-overridden inherited property?
if (property.activeInStyle() && cssMetadata().isPropertyInherited(property.name)) {
return true;
}
}
return false;
}
function cleanUserAgentPayload(payload: Protocol.CSS.RuleMatch[]): Protocol.CSS.RuleMatch[] {
for (const ruleMatch of payload) {
cleanUserAgentSelectors(ruleMatch);
}
// Merge UA rules that are sequential and have similar selector/media.
const cleanMatchedPayload = [];
for (const ruleMatch of payload) {
const lastMatch = cleanMatchedPayload[cleanMatchedPayload.length - 1];
if (!lastMatch || ruleMatch.rule.origin !== 'user-agent' || lastMatch.rule.origin !== 'user-agent' ||
ruleMatch.rule.selectorList.text !== lastMatch.rule.selectorList.text ||
mediaText(ruleMatch) !== mediaText(lastMatch)) {
cleanMatchedPayload.push(ruleMatch);
continue;
}
mergeRule(ruleMatch, lastMatch);
}
return cleanMatchedPayload;
function mergeRule(from: Protocol.CSS.RuleMatch, to: Protocol.CSS.RuleMatch): void {
const shorthands = (new Map() as Map<string, string>);
const properties = (new Map() as Map<string, string>);
for (const entry of to.rule.style.shorthandEntries) {
shorthands.set(entry.name, entry.value);
}
for (const entry of to.rule.style.cssProperties) {
properties.set(entry.name, entry.value);
}
for (const entry of from.rule.style.shorthandEntries) {
shorthands.set(entry.name, entry.value);
}
for (const entry of from.rule.style.cssProperties) {
properties.set(entry.name, entry.value);
}
to.rule.style.shorthandEntries = [...shorthands.entries()].map(([name, value]) => ({name, value}));
to.rule.style.cssProperties = [...properties.entries()].map(([name, value]) => ({name, value}));
}
function mediaText(ruleMatch: Protocol.CSS.RuleMatch): string|null {
if (!ruleMatch.rule.media) {
return null;
}
return ruleMatch.rule.media.map(media => media.text).join(', ');
}
function cleanUserAgentSelectors(ruleMatch: Protocol.CSS.RuleMatch): void {
const {matchingSelectors, rule} = ruleMatch;
if (rule.origin !== 'user-agent' || !matchingSelectors.length) {
return;
}
rule.selectorList.selectors = rule.selectorList.selectors.filter((item, i) => matchingSelectors.includes(i));
rule.selectorList.text = rule.selectorList.selectors.map(item => item.text).join(', ');
ruleMatch.matchingSelectors = matchingSelectors.map((item, i) => i);
}
}
/**
* Return a mapping of the highlight names in the specified RuleMatch to
* the indices of selectors in that selector list with that highlight name.
*
* For example, consider the following ruleset:
* span::highlight(foo), div, #mySpan::highlight(bar), .highlighted::highlight(foo) {
* color: blue;
* }
*
* For a <span id="mySpan" class="highlighted"></span>, a RuleMatch for that span
* would have matchingSelectors [0, 2, 3] indicating that the span
* matches all of the highlight selectors.
*
* For that RuleMatch, this function would produce the following map:
* {
* "foo": [0, 3],
* "bar": [2]
* }
*
* @param ruleMatch
* @returns A mapping of highlight names to lists of indices into the selector
* list associated with ruleMatch. The indices correspond to the selectors in the rule
* associated with the key's highlight name.
*/
function customHighlightNamesToMatchingSelectorIndices(ruleMatch: Protocol.CSS.RuleMatch): Map<string, number[]> {
const highlightNamesToMatchingSelectors = new Map<string, number[]>();
for (let i = 0; i < ruleMatch.matchingSelectors.length; i++) {
const matchingSelectorIndex = ruleMatch.matchingSelectors[i];
const selectorText = ruleMatch.rule.selectorList.selectors[matchingSelectorIndex].text;
const highlightNameMatch = selectorText.match(/::highlight\((.*)\)/);
if (highlightNameMatch) {
const highlightName = highlightNameMatch[1];
const selectorsForName = highlightNamesToMatchingSelectors.get(highlightName);
if (selectorsForName) {
selectorsForName.push(matchingSelectorIndex);
} else {
highlightNamesToMatchingSelectors.set(highlightName, [matchingSelectorIndex]);
}
}
}
return highlightNamesToMatchingSelectors;
}
function queryMatches(style: CSSStyleDeclaration): boolean {
if (!style.parentRule) {
return true;
}
const parentRule = style.parentRule as CSSStyleRule;
const queries = [...parentRule.media, ...parentRule.containerQueries, ...parentRule.supports, ...parentRule.scopes];
for (const query of queries) {
if (!query.active()) {
return false;
}
}
return true;
}
export interface CSSMatchedStylesPayload {
cssModel: CSSModel;
node: DOMNode;
activePositionFallbackIndex: number;
inlinePayload: Protocol.CSS.CSSStyle|null;
attributesPayload: Protocol.CSS.CSSStyle|null;
matchedPayload: Protocol.CSS.RuleMatch[];
pseudoPayload: Protocol.CSS.PseudoElementMatches[];
inheritedPayload: Protocol.CSS.InheritedStyleEntry[];
inheritedPseudoPayload: Protocol.CSS.InheritedPseudoElementMatches[];
animationsPayload: Protocol.CSS.CSSKeyframesRule[];
parentLayoutNodeId: Protocol.DOM.NodeId|undefined;
positionTryRules: Protocol.CSS.CSSPositionTryRule[];
propertyRules: Protocol.CSS.CSSPropertyRule[];
cssPropertyRegistrations: Protocol.CSS.CSSPropertyRegistration[];
fontPaletteValuesRule: Protocol.CSS.CSSFontPaletteValuesRule|undefined;
animationStylesPayload: Protocol.CSS.CSSAnimationStyle[];
transitionsStylePayload: Protocol.CSS.CSSStyle|null;
inheritedAnimatedPayload: Protocol.CSS.InheritedAnimatedStyleEntry[];
}
export class CSSRegisteredProperty {
#registration: Protocol.CSS.CSSPropertyRegistration|CSSPropertyRule;
#cssModel: CSSModel;
#style: CSSStyleDeclaration|undefined;
constructor(cssModel: CSSModel, registration: CSSPropertyRule|Protocol.CSS.CSSPropertyRegistration) {
this.#cssModel = cssModel;
this.#registration = registration;
}
isAtProperty(): boolean {
return this.#registration instanceof CSSPropertyRule;
}
propertyName(): string {
return this.#registration instanceof CSSPropertyRule ? this.#registration.propertyName().text :
this.#registration.propertyName;
}
initialValue(): string|null {
return this.#registration instanceof CSSPropertyRule ? this.#registration.initialValue() :
this.#registration.initialValue?.text ?? null;
}
inherits(): boolean {
return this.#registration instanceof CSSPropertyRule ? this.#registration.inherits() : this.#registration.inherits;
}
syntax(): string {
return this.#registration instanceof CSSPropertyRule ? this.#registration.syntax() :
`"${this.#registration.syntax}"`;
}
#asCSSProperties(): Protocol.CSS.CSSProperty[] {
if (this.#registration instanceof CSSPropertyRule) {
return [];
}
const {inherits, initialValue, syntax} = this.#registration;
const properties = [
{name: 'inherits', value: `${inherits}`},
{name: 'syntax', value: `"${syntax}"`},
];
if (initialValue !== undefined) {
properties.push({name: 'initial-value', value: initialValue.text});
}
return properties;
}
style(): CSSStyleDeclaration {
if (!this.#style) {
this.#style = this.#registration instanceof CSSPropertyRule ?
this.#registration.style :
new CSSStyleDeclaration(
this.#cssModel, null, {cssProperties: this.#asCSSProperties(), shorthandEntries: []}, Type.Pseudo);
}
return this.#style;
}
}
export class CSSMatchedStyles {
#cssModelInternal: CSSModel;
#nodeInternal: DOMNode;
#addedStyles: Map<CSSStyleDeclaration, DOMNode>;
#matchingSelectors: Map<number, Map<string, boolean>>;
#keyframesInternal: CSSKeyframesRule[];
#registeredProperties: CSSRegisteredProperty[];
#registeredPropertyMap = new Map<string, CSSRegisteredProperty>();
#nodeForStyleInternal: Map<CSSStyleDeclaration, DOMNode|null>;
#inheritedStyles: Set<CSSStyleDeclaration>;
#styleToDOMCascade: Map<CSSStyleDeclaration, DOMInheritanceCascade>;
#parentLayoutNodeId: Protocol.DOM.NodeId|undefined;
#positionTryRules: CSSPositionTryRule[];
#activePositionFallbackIndex: number;
#mainDOMCascade?: DOMInheritanceCascade;
#pseudoDOMCascades?: Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>;
#customHighlightPseudoDOMCascades?: Map<string, DOMInheritanceCascade>;
readonly #fontPaletteValuesRule: CSSFontPaletteValuesRule|undefined;
static async create(payload: CSSMatchedStylesPayload): Promise<CSSMatchedStyles> {
const cssMatchedStyles = new CSSMatchedStyles(payload);
await cssMatchedStyles.init(payload);
return cssMatchedStyles;
}
private constructor({
cssModel,
node,
animationsPayload,
parentLayoutNodeId,
positionTryRules,
propertyRules,
cssPropertyRegistrations,
fontPaletteValuesRule,
activePositionFallbackIndex,
}: CSSMatchedStylesPayload) {
this.#cssModelInternal = cssModel;
this.#nodeInternal = node;
this.#addedStyles = new Map();
this.#matchingSelectors = new Map();
this.#registeredProperties = [
...propertyRules.map(rule => new CSSPropertyRule(cssModel, rule)),
...cssPropertyRegistrations,
].map(r => new CSSRegisteredProperty(cssModel, r));
this.#keyframesInternal = [];
if (animationsPayload) {
this.#keyframesInternal = animationsPayload.map(rule => new CSSKeyframesRule(cssModel, rule));
}
this.#positionTryRules = positionTryRules.map(rule => new CSSPositionTryRule(cssModel, rule));
this.#parentLayoutNodeId = parentLayoutNodeId;
this.#fontPaletteValuesRule =
fontPaletteValuesRule ? new CSSFontPaletteValuesRule(cssModel, fontPaletteValuesRule) : undefined;
this.#nodeForStyleInternal = new Map();
this.#inheritedStyles = new Set();
this.#styleToDOMCascade = new Map();
this.#registeredPropertyMap = new Map();
this.#activePositionFallbackIndex = activePositionFallbackIndex;
}
private async init({
matchedPayload,
inheritedPayload,
inlinePayload,
attributesPayload,
pseudoPayload,
inheritedPseudoPayload,
animationStylesPayload,
transitionsStylePayload,
inheritedAnimatedPayload,
}: CSSMatchedStylesPayload): Promise<void> {
matchedPayload = cleanUserAgentPayload(matchedPayload);
for (const inheritedResult of inheritedPayload) {
inheritedResult.matchedCSSRules = cleanUserAgentPayload(inheritedResult.matchedCSSRules);
}
this.#mainDOMCascade = await this.buildMainCascade(
inlinePayload, attributesPayload, matchedPayload, inheritedPayload, animationStylesPayload,
transitionsStylePayload, inheritedAnimatedPayload);
[this.#pseudoDOMCascades, this.#customHighlightPseudoDOMCascades] =
this.buildPseudoCascades(pseudoPayload, inheritedPseudoPayload);
for (const domCascade of Array.from(this.#customHighlightPseudoDOMCascades.values())
.concat(Array.from(this.#pseudoDOMCascades.values()))
.concat(this.#mainDOMCascade)) {
for (const style of domCascade.styles()) {
this.#styleToDOMCascade.set(style, domCascade);
}
}
for (const prop of this.#registeredProperties) {
this.#registeredPropertyMap.set(prop.propertyName(), prop);
}
}
private async buildMainCascade(
inlinePayload: Protocol.CSS.CSSStyle|null,
attributesPayload: Protocol.CSS.CSSStyle|null,
matchedPayload: Protocol.CSS.RuleMatch[],
inheritedPayload: Protocol.CSS.InheritedStyleEntry[],
animationStylesPayload: Protocol.CSS.CSSAnimationStyle[],
transitionsStylePayload: Protocol.CSS.CSSStyle|null,
inheritedAnimatedPayload: Protocol.CSS.InheritedAnimatedStyleEntry[],
): Promise<DOMInheritanceCascade> {
const nodeCascades: NodeCascade[] = [];
const nodeStyles: CSSStyleDeclaration[] = [];
function addAttributesStyle(this: CSSMatchedStyles): void {
if (!attributesPayload) {
return;
}
const style = new CSSStyleDeclaration(this.#cssModelInternal, null, attributesPayload, Type.Attributes);
this.#nodeForStyleInternal.set(style, this.#nodeInternal);
nodeStyles.push(style);
}
// Transition styles take precedence over animation styles & inline styles.
if (transitionsStylePayload) {
const style = new CSSStyleDeclaration(this.#cssModelInternal, null, transitionsStylePayload, Type.Transition);
this.#nodeForStyleInternal.set(style, this.#nodeInternal);
nodeStyles.push(style);
}
// Animation styles take precedence over inline styles.
for (const animationsStyle of animationStylesPayload) {
const style = new CSSStyleDeclaration(
this.#cssModelInternal, null, animationsStyle.style, Type.Animation, animationsStyle.name);
this.#nodeForStyleInternal.set(style, this.#nodeInternal);
nodeStyles.push(style);
}
// Inline style takes precedence over regular and inherited rules.
if (inlinePayload && this.#nodeInternal.nodeType() === Node.ELEMENT_NODE) {
const style = new CSSStyleDeclaration(this.#cssModelInternal, null, inlinePayload, Type.Inline);
this.#nodeForStyleInternal.set(style, this.#nodeInternal);
nodeStyles.push(style);
}
// Add rules in reverse order to match the cascade order.
let addedAttributesStyle;
for (let i = matchedPayload.length - 1; i >= 0; --i) {
const rule = new CSSStyleRule(this.#cssModelInternal, matchedPayload[i].rule);
if ((rule.isInjected() || rule.isUserAgent()) && !addedAttributesStyle) {
// Show element's Style Attributes after all author rules.
addedAttributesStyle = true;
addAttributesStyle.call(this);
}
this.#nodeForStyleInternal.set(rule.style, this.#nodeInternal);
nodeStyles.push(rule.style);
this.addMatchingSelectors(this.#nodeInternal, rule, matchedPayload[i].matchingSelectors);
}
if (!addedAttributesStyle) {
addAttributesStyle.call(this);
}
nodeCascades.push(new NodeCascade(this, nodeStyles, false /* #isInherited */));
// Walk the node structure and identify styles with inherited properties.
let parentNode: (DOMNode|null) = this.#nodeInternal.parentNode;
const traverseParentInFlatTree = async(node: DOMNode): Promise<DOMNode|null> => {
if (node.hasAssignedSlot()) {
return await node.assignedSlot?.deferredNode.resolvePromise() ?? null;
}
return node.parentNode;
};
for (let i = 0; parentNode && inheritedPayload && i < inheritedPayload.length; ++i) {
const inheritedStyles = [];
const entryPayload = inheritedPayload[i];
const inheritedAnimatedEntryPayload = inheritedAnimatedPayload[i];
const inheritedInlineStyle = entryPayload.inlineStyle ?
new CSSStyleDeclaration(this.#cssModelInternal, null, entryPayload.inlineStyle, Type.Inline) :
null;
const inheritedTransitionsStyle = inheritedAnimatedEntryPayload?.transitionsStyle ?
new CSSStyleDeclaration(
this.#cssModelInternal, null, inheritedAnimatedEntryPayload?.transitionsStyle, Type.Transition) :
null;
const inheritedAnimationStyles =
inheritedAnimatedEntryPayload?.animationStyles?.map(
animationStyle => new CSSStyleDeclaration(
this.#cssModelInternal, null, animationStyle.style, Type.Animation, animationStyle.name)) ??
[];
if (inheritedTransitionsStyle && containsInherited(inheritedTransitionsStyle)) {
this.#nodeForStyleInternal.set(inheritedTransitionsStyle, parentNode);
inheritedStyles.push(inheritedTransitionsStyle);
this.#inheritedStyles.add(inheritedTransitionsStyle);
}
for (const inheritedAnimationsStyle of inheritedAnimationStyles) {
if (!containsInherited(inheritedAnimationsStyle)) {
continue;
}
this.#nodeForStyleInternal.set(inheritedAnimationsStyle, parentNode);
inheritedStyles.push(inheritedAnimationsStyle);
this.#inheritedStyles.add(inheritedAnimationsStyle);
}
if (inheritedInlineStyle && containsInherited(inheritedInlineStyle)) {
this.#nodeForStyleInternal.set(inheritedInlineStyle, parentNode);
inheritedStyles.push(inheritedInlineStyle);
this.#inheritedStyles.add(inheritedInlineStyle);
}
const inheritedMatchedCSSRules = entryPayload.matchedCSSRules || [];
for (let j = inheritedMatchedCSSRules.length - 1; j >= 0; --j) {
const inheritedRule = new CSSStyleRule(this.#cssModelInternal, inheritedMatchedCSSRules[j].rule);
this.addMatchingSelectors(parentNode, inheritedRule, inheritedMatchedCSSRules[j].matchingSelectors);
if (!containsInherited(inheritedRule.style)) {
continue;
}
if (!containsCustomProperties(inheritedRule.style)) {
if (containsStyle(nodeStyles, inheritedRule.style) ||
containsStyle(this.#inheritedStyles, inheritedRule.style)) {
continue;
}
}
this.#nodeForStyleInternal.set(inheritedRule.style, parentNode);
inheritedStyles.push(inheritedRule.style);
this.#inheritedStyles.add(inheritedRule.style);
}
parentNode = await traverseParentInFlatTree(parentNode);
nodeCascades.push(new NodeCascade(this, inheritedStyles, true /* #isInherited */));
}
return new DOMInheritanceCascade(nodeCascades, this.#registeredProperties);
}
/**
* Pseudo rule matches received via the inspector protocol are grouped by pseudo type.
* For custom highlight pseudos, we need to instead group the rule matches by highlight
* name in order to produce separate cascades for each highlight name. This is necessary
* so that styles of ::highlight(foo) are not shown as overriding styles of ::highlight(bar).
*
* This helper function takes a list of rule matches and generates separate NodeCascades
* for each custom highlight name that was matched.
*/
private buildSplitCustomHighlightCascades(
rules: Protocol.CSS.RuleMatch[], node: DOMNode, isInherited: boolean,
pseudoCascades: Map<string, NodeCascade[]>): void {
const splitHighlightRules = new Map<string, CSSStyleDeclaration[]>();
for (let j = rules.length - 1; j >= 0; --j) {
const highlightNamesToMatchingSelectorIndices = customHighlightNamesToMatchingSelectorIndices(rules[j]);
for (const [highlightName, matchingSelectors] of highlightNamesToMatchingSelectorIndices) {
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[j].rule);
this.#nodeForStyleInternal.set(pseudoRule.style, node);
if (isInherited) {
this.#inheritedStyles.add(pseudoRule.style);
}
this.addMatchingSelectors(node, pseudoRule, matchingSelectors);
const ruleListForHighlightName = splitHighlightRules.get(highlightName);
if (ruleListForHighlightName) {
ruleListForHighlightName.push(pseudoRule.style);
} else {
splitHighlightRules.set(highlightName, [pseudoRule.style]);
}
}
}
for (const [highlightName, highlightStyles] of splitHighlightRules) {
const nodeCascade = new NodeCascade(this, highlightStyles, isInherited, true /* #isHighlightPseudoCascade*/);
const cascadeListForHighlightName = pseudoCascades.get(highlightName);
if (cascadeListForHighlightName) {
cascadeListForHighlightName.push(nodeCascade);
} else {
pseudoCascades.set(highlightName, [nodeCascade]);
}
}
}
private buildPseudoCascades(
pseudoPayload: Protocol.CSS.PseudoElementMatches[],
inheritedPseudoPayload: Protocol.CSS.InheritedPseudoElementMatches[]):
[Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>, Map<string, DOMInheritanceCascade>] {
const pseudoInheritanceCascades = new Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>();
const customHighlightPseudoInheritanceCascades = new Map<string, DOMInheritanceCascade>();
if (!pseudoPayload) {
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
}
const pseudoCascades = new Map<Protocol.DOM.PseudoType, NodeCascade[]>();
const customHighlightPseudoCascades = new Map<string, NodeCascade[]>();
for (let i = 0; i < pseudoPayload.length; ++i) {
const entryPayload = pseudoPayload[i];
// PseudoElement nodes are not created unless "content" css property is set.
const pseudoElement = this.#nodeInternal.pseudoElements().get(entryPayload.pseudoType)?.at(-1) || null;
const pseudoStyles = [];
const rules = entryPayload.matches || [];
if (entryPayload.pseudoType === Protocol.DOM.PseudoType.Highlight) {
this.buildSplitCustomHighlightCascades(
rules, this.#nodeInternal, false /* #isInherited */, customHighlightPseudoCascades);
} else {
for (let j = rules.length - 1; j >= 0; --j) {
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[j].rule);
pseudoStyles.push(pseudoRule.style);
const nodeForStyle =
cssMetadata().isHighlightPseudoType(entryPayload.pseudoType) ? this.#nodeInternal : pseudoElement;
this.#nodeForStyleInternal.set(pseudoRule.style, nodeForStyle);
if (nodeForStyle) {
this.addMatchingSelectors(nodeForStyle, pseudoRule, rules[j].matchingSelectors);
}
}
const isHighlightPseudoCascade = cssMetadata().isHighlightPseudoType(entryPayload.pseudoType);
const nodeCascade = new NodeCascade(
this, pseudoStyles, false /* #isInherited */, isHighlightPseudoCascade /* #isHighlightPseudoCascade*/);
pseudoCascades.set(entryPayload.pseudoType, [nodeCascade]);
}
}
if (inheritedPseudoPayload) {
let parentNode: (DOMNode|null) = this.#nodeInternal.parentNode;
for (let i = 0; parentNode && i < inheritedPseudoPayload.length; ++i) {
const inheritedPseudoMatches = inheritedPseudoPayload[i].pseudoElements;
for (let j = 0; j < inheritedPseudoMatches.length; ++j) {
const inheritedEntryPayload = inheritedPseudoMatches[j];
const rules = inheritedEntryPayload.matches || [];
if (inheritedEntryPayload.pseudoType === Protocol.DOM.PseudoType.Highlight) {
this.buildSplitCustomHighlightCascades(
rules, parentNode, true /* #isInherited */, customHighlightPseudoCascades);
} else {
const pseudoStyles = [];
for (let k = rules.length - 1; k >= 0; --k) {
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[k].rule);
pseudoStyles.push(pseudoRule.style);
this.#nodeForStyleInternal.set(pseudoRule.style, parentNode);
this.#inheritedStyles.add(pseudoRule.style);
this.addMatchingSelectors(parentNode, pseudoRule, rules[k].matchingSelectors);
}
const isHighlightPseudoCascade = cssMetadata().isHighlightPseudoType(inheritedEntryPayload.pseudoType);
const nodeCascade = new NodeCascade(
this, pseudoStyles, true /* #isInherited */, isHighlightPseudoCascade /* #isHighlightPseudoCascade*/);
const cascadeListForPseudoType = pseudoCascades.get(inheritedEntryPayload.pseudoType);
if (cascadeListForPseudoType) {
cascadeListForPseudoType.push(nodeCascade);
} else {
pseudoCascades.set(inheritedEntryPayload.pseudoType, [nodeCascade]);
}
}
}
parentNode = parentNode.parentNode;
}
}
// Now that we've built the arrays of NodeCascades for each pseudo type, convert them into
// DOMInheritanceCascades.
for (const [pseudoType, nodeCascade] of pseudoCascades.entries()) {
pseudoInheritanceCascades.set(pseudoType, new DOMInheritanceCascade(nodeCascade, this.#registeredProperties));
}
for (const [highlightName, nodeCascade] of customHighlightPseudoCascades.entries()) {
customHighlightPseudoInheritanceCascades.set(
highlightName, new DOMInheritanceCascade(nodeCascade, this.#registeredProperties));
}
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
}
private addMatchingSelectors(
this: CSSMatchedStyles, node: DOMNode, rule: CSSStyleRule, matchingSelectorIndices: number[]): void {
for (const matchingSelectorIndex of matchingSelectorIndices) {
const selector = rule.selectors[matchingSelectorIndex];
if (selector) {
this.setSelectorMatches(node, selector.text, true);
}
}
}
node(): DOMNode {
return this.#nodeInternal;
}
cssModel(): CSSModel {
return this.#cssModelInternal;
}
hasMatchingSelectors(rule: CSSStyleRule): boolean {
return (rule.selectors.length === 0 || this.getMatchingSelectors(rule).length > 0) && queryMatches(rule.style);
}
getParentLayoutNodeId(): Protocol.DOM.NodeId|undefined {
return this.#parentLayoutNodeId;
}
getMatchingSelectors(rule: CSSStyleRule): number[] {
const node = this.nodeForStyle(rule.style);
if (!node || typeof node.id !== 'number') {
return [];
}
const map = this.#matchingSelectors.get(node.id);
if (!map) {
return [];
}
const result = [];
for (let i = 0; i < rule.selectors.length; ++i) {
if (map.get(rule.selectors[i].text)) {
result.push(i);
}
}
return result;
}
async recomputeMatchingSelectors(rule: CSSStyleRule): Promise<void> {
const node = this.nodeForStyle(rule.style);
if (!node) {
return;
}
const promises = [];
for (const selector of rule.selectors) {
promises.push(querySelector.call(this, node, selector.text));
}
await Promise.all(promises);
async function querySelector(this: CSSMatchedStyles, node: DOMNode, selectorText: string): Promise<void> {
const ownerDocument = node.ownerDocument;
if (!ownerDocument) {
return;
}
// We assume that "matching" property does not ever change during the
// MatchedStyleResult's lifetime.
if (typeof node.id === 'number') {
const map = this.#matchingSelectors.get(node.id);
if (map && map.has(selectorText)) {
return;
}
}
if (typeof ownerDocument.id !== 'number') {
return;
}
const matchingNodeIds = await this.#nodeInternal.domModel().querySelectorAll(ownerDocument.id, selectorText);
if (matchingNodeIds) {
if (typeof node.id === 'number') {
this.setSelectorMatches(node, selectorText, matchingNodeIds.indexOf(node.id) !== -1);
} else {
this.setSelectorMatches(node, selectorText, false);
}
}
}
}
addNewRule(rule: CSSStyleRule, node: DOMNode): Promise<void> {
this.#addedStyles.set(rule.style, node);
return this.recomputeMatchingSelectors(rule);
}
private setSelectorMatches(node: DOMNode, selectorText: string, value: boolean): void {
if (typeof node.id !== 'number') {
return;
}
let map = this.#matchingSelectors.get(node.id);
if (!map) {
map = new Map();
this.#matchingSelectors.set(node.id, map);
}
map.set(selectorText, value);
}
nodeStyles(): CSSStyleDeclaration[] {
Platform.assertNotNullOrUndefined(this.#mainDOMCascade);
return this.#mainDOMCascade.styles();
}
inheritedStyles(): CSSStyleDeclaration[] {
return this.#mainDOMCascade?.styles().filter(style => this.isInherited(style)) ?? [];
}
animationStyles(): CSSStyleDeclaration[] {
return this.#mainDOMCascade?.styles().filter(style => !this.isInherited(style) && style.type === Type.Animation) ??
[];
}
transitionsStyle(): CSSStyleDeclaration|null {
return this.#mainDOMCascade?.styles().find(style => !this.isInherited(style) && style.type === Type.Transition) ??
null;
}
registeredProperties(): CSSRegisteredProperty[] {
return this.#registeredProperties;
}
getRegisteredProperty(name: string): CSSRegisteredProperty|undefined {
return this.#registeredPropertyMap.get(name);
}
fontPaletteValuesRule(): CSSFontPaletteValuesRule|undefined {
return this.#fontPaletteValuesRule;
}
keyframes(): CSSKeyframesRule[] {
return this.#keyframesInternal;
}
positionTryRules(): CSSPositionTryRule[] {
return this.#positionTryRules;
}
activePositionFallbackIndex(): number {
return this.#activePositionFallbackIndex;
}
pseudoStyles(pseudoType: Protocol.DOM.PseudoType): CSSStyleDeclaration[] {
Platform.assertNotNullOrUndefined(this.#pseudoDOMCascades);
const domCascade = this.#pseudoDOMCascades.get(pseudoType);
return domCascade ? domCascade.styles() : [];
}
pseudoTypes(): Set<Protocol.DOM.PseudoType> {
Platform.assertNotNullOrUndefined(this.#pseudoDOMCascades);
return new Set(this.#pseudoDOMCascades.keys());
}
customHighlightPseudoStyles(highlightName: string): CSSStyleDeclaration[] {
Platform.assertNotNullOrUndefined(this.#customHighlightPseudoDOMCascades);
const domCascade = this.#customHighlightPseudoDOMCascades.get(highlightName);
return domCascade ? domCascade.styles() : [];
}
customHighlightPseudoNames(): Set<string> {
Platform.assertNotNullOrUndefined(this.#customHighlightPseudoDOMCascades);
return new Set(this.#customHighlightPseudoDOMCascades.keys());
}
nodeForStyle(style: CSSStyleDeclaration): DOMNode|null {
return this.#addedStyles.get(style) || this.#nodeForStyleInternal.get(style) || null;
}
availableCSSVariables(style: CSSStyleDeclaration): string[] {
const domCascade = this.#styleToDOMCascade.get(style);
return domCascade ? domCascade.findAvailableCSSVariables(style) : [];
}
computeCSSVariable(style: CSSStyleDeclaration, variableName: string): CSSVariableValue|null {
const domCascade = this.#styleToDOMCascade.get(style);
return domCascade ? domCascade.computeCSSVariable(style, variableName) : null;
}
resolveGlobalKeyword(property: CSSProperty, keyword: CSSWideKeyword): CSSValueSource|null {
const resolved = this.#styleToDOMCascade.get(property.ownerStyle)?.resolveGlobalKeyword(property, keyword);
return resolved ? new CSSValueSource(resolved) : null;
}
isInherited(style: CSSStyleDeclaration): boolean {
return this.#inheritedStyles.has(style);
}
propertyState(property: CSSProperty): PropertyState|null {
const domCascade = this.#styleToDOMCascade.get(property.ownerStyle);
return domCascade ? domCascade.propertyState(property) : null;
}
resetActiveProperties(): void {
Platform.assertNotNullOrUndefined(this.#mainDOMCascade);
Platform.assertNotNullOrUndefined(this.#pseudoDOMCascades);
Platform.assertNotNullOrUndefined(this.#customHighlightPseudoDOMCascades);
this.#mainDOMCascade.reset();
for (const domCascade of this.#pseudoDOMCascades.values()) {
domCascade.reset();
}
for (const domCascade of this.#customHighlightPseudoDOMCascades.values()) {
domCascade.reset();
}
}
}
class NodeCascade {
#matchedStyles: CSSMatchedStyles;
readonly styles: CSSStyleDeclaration[];
readonly #isInherited: boolean;
readonly #isHighlightPseudoCascade: boolean;
readonly propertiesState: Map<CSSProperty, PropertyState>;
readonly activeProperties: Map<string, CSSProperty>;
constructor(
matchedStyles: CSSMatchedStyles, styles: CSSStyleDeclaration[], isInherited: boolean,
isHighlightPseudoCascade: boolean = false) {
this.#matchedStyles = matchedStyles;
this.styles = styles;
this.#isInherited = isInherited;
this.#isHighlightPseudoCascade = isHighlightPseudoCascade;
this.propertiesState = new Map();
this.activeProperties = new Map();
}
computeActiveProperties(): void {
this.propertiesState.clear();
this.activeProperties.clear();
for (let i = this.styles.length - 1; i >= 0; i--) {
const style = this.styles[i];
const rule = style.parentRule;
// Compute cascade for CSSStyleRules only.
if (rule && !(rule instanceof CSSStyleRule)) {
continue;
}
if (rule && !this.#matchedStyles.hasMatchingSelectors(rule)) {
continue;
}
for (const property of style.allProperties()) {
// Do not pick non-inherited properties from inherited styles.
const metadata = cssMetadata();
// All properties are inherited for highlight pseudos.
if (this.#isInherited && !this.#isHighlightPseudoCascade && !metadata.isPropertyInherited(property.name)) {
continue;
}
// When a property does not have a range in an otherwise ranged CSSStyleDeclaration,
// we consider it as a non-leading property (see computeLeadingProperties()), and most
// of them are computed longhands. We exclude these from activeProperties calculation,
// and use parsed longhands instead (see below).
if (style.range && !property.range) {
continue;
}
if (!property.activeInStyle()) {
this.propertiesState.set(property, PropertyState.OVERLOADED);
continue;
}
// If the custom property was registered with `inherits: false;`, inherited properties are invalid.
if (this.#isInherited) {
const registration = this.#matchedStyles.getRegisteredProperty(property.name);
if (registration && !registration.inherits()) {
this.propertiesState.set(property, PropertyState.OVERLOADED);
continue;
}
}
const canonicalName = metadata.canonicalPropertyName(property.name);
this.updatePropertyState(property, canonicalName);
for (const longhand of property.getLonghandProperties()) {
if (metadata.isCSSPropertyName(longhand.name)) {
this.updatePropertyState(longhand, longhand.name);
}
}
}
}
}
private updatePropertyState(propertyWithHigherSpecificity: CSSProperty, canonicalName: string): void {
const activeProperty = this.activeProperties.get(canonicalName);
if (activeProperty?.important && !propertyWithHigherSpecificity.important) {
this.propertiesState.set(propertyWithHigherSpecificity, PropertyState.OVERLOADED);
return;
}
if (activeProperty) {
this.propertiesState.set(activeProperty, PropertyState.OVERLOADED);
}
this.propertiesState.set(propertyWithHigherSpecificity, PropertyState.ACTIVE);
this.activeProperties.set(canonicalName, propertyWithHigherSpecificity);
}
}
function isRegular(declaration: CSSProperty|CSSRegisteredProperty): declaration is CSSProperty {
return 'ownerStyle' in declaration;
}
export class CSSValueSource {
readonly declaration: CSSProperty|CSSRegisteredProperty;
constructor(declaration: CSSProperty|CSSRegisteredProperty) {
this.declaration = declaration;
}
get value(): string|null {
return isRegular(this.declaration) ? this.declaration.value : this.declaration.initialValue();
}
get style(): CSSStyleDeclaration {
return isRegular(this.declaration) ? this.declaration.ownerStyle : this.declaration.style();
}
get name(): string {
return isRegular(this.declaration) ? this.declaration.name : this.declaration.propertyName();
}
}
export interface CSSVariableValue {
value: string;
declaration: CSSValueSource;
}
class SCCRecordEntry {
private rootDiscoveryTime: number;
get isRootEntry(): boolean {
return this.rootDiscoveryTime === this.discoveryTime;
}
updateRoot(neighbor: SCCRecordEntry): void {
this.rootDiscoveryTime = Math.min(this.rootDiscoveryTime, neighbor.rootDiscoveryTime);
}
constructor(readonly nodeCascade: NodeCascade, readonly name: string, private readonly discoveryTime: number) {
this.rootDiscoveryTime = discoveryTime;
}
}
class SCCRecord {
#time = 0;
#stack: SCCRecordEntry[] = [];
#entries = new Map<NodeCascade, Map<string, SCCRecordEntry>>();
get(nodeCascade: NodeCascade, variable: string): SCCRecordEntry|undefined {
return this.#entries.get(nodeCascade)?.get(variable);
}
add(nodeCascade: NodeCascade, variable: string): SCCRecordEntry {
const existing = this.get(nodeCascade, variable);
if (existing) {
return existing;
}
const entry = new SCCRecordEntry(nodeCascade, variable, this.#time++);
this.#stack.push(entry);
let map = this.#entries.get(nodeCascade);
if (!map) {
map = new Map();
this.#entries.set(nodeCascade, map);
}
map.set(variable, entry);
return entry;
}
isInInProgressSCC(childRecord: SCCRecordEntry): boolean {
return this.#stack.includes(childRecord);
}
finishSCC(root: SCCRecordEntry): SCCRecordEntry[] {
const startIndex = this.#stack.lastIndexOf(root);
console.assert(startIndex >= 0, 'Root is not an in-progress scc');
return this.#stack.splice(startIndex);
}
}
function* forEach<T>(array: T[], startAfter?: T): Generator<T> {
const startIdx = startAfter !== undefined ? array.indexOf(startAfter) + 1 : 0;
for (let i = startIdx; i < array.length; ++i) {
yield array[i];
}
}
class DOMInheritanceCascade {
readonly #nodeCascades: NodeCascade[];
readonly #propertiesState: Map<CSSProperty, PropertyState>;
readonly #availableCSSVariables: Map<NodeCascade, Map<string, CSSVariableValue|null>>;
readonly #computedCSSVariables: Map<NodeCascade, Map<string, CSSVariableValue|null>>;
#initialized: boolean;
readonly #styleToNodeCascade: Map<CSSStyleDeclaration, NodeCascade>;
#registeredProperties: CSSRegisteredProperty[];
constructor(nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[]) {
this.#nodeCascades = nodeCascades;
this.#propertiesState = new Map();
this.#availableCSSVariables = new Map();
this.#computedCSSVariables = new Map();
this.#initialized = false;
this.#registeredProperties = registeredProperties;
this.#styleToNodeCascade = new Map();
for (const nodeCascade of nodeCascades) {
for (const style of nodeCascade.styles) {
this.#styleToNodeCascade.set(style, nodeCascade);
}
}
}
findAvailableCSSVariables(style: CSSStyleDeclaration): string[] {
const nodeCascade = this.#styleToNodeCascade.get(style);
if (!nodeCascade) {
return [];
}
this.ensureInitialized();
const availableCSSVariables = this.#availableCSSVariables.get(nodeCascade);
if (!availableCSSVariables) {
return [];
}
return Array.from(availableCSSVariables.keys());
}
#findPropertyInPreviousStyle(property: CSSProperty, filter: (property: CSSProperty) => boolean): CSSProperty|null {
const cascade = this.#styleToNodeCascade.get(property.ownerStyle);
if (!cascade) {
return null;
}
for (const style of forEach(cascade.styles, property.ownerStyle)) {
const candidate =
style.allProperties().findLast(candidate => candidate.name === property.name && filter(candidate));
if (candidate) {
return candidate;
}
}
return null;
}
#findPropertyInParentCascade(property: CSSProperty): CSSProperty|null {
const nodeCascade = this.#styleToNodeCascade.get(property.ownerStyle);
if (!nodeCascade) {
return null;
}
for (const cascade of forEach(this.#nodeCascades, nodeCascade)) {
for (const style of cascade.styles) {
const inheritedProperty =
style.allProperties().findLast(inheritedProperty => inheritedProperty.name === property.name);
if (inheritedProperty) {
return inheritedProperty;
}
}
}
return null;
}
#findPropertyInParentCascadeIfInherited(property: CSSProperty): CSSProperty|null {
if (!cssMetadata().isPropertyInherited(property.name) ||
!(this.#findCustomPropertyRegistration(property)?.inherits() ?? true)) {
return null;
}
return this.#findPropertyInParentCascade(property);
}
#findCustomPropertyRegistration(property: CSSProperty): CSSRegisteredProperty|null {
const registration = this.#registeredProperties.find(registration => registration.propertyName() === property.name);
return registration ? registration : null;
}
resolveGlobalKeyword(property: CSSProperty, keyword: CSSWideKeyword): null|CSSProperty|CSSRegisteredProperty {
const isPreviousLayer = (other: CSSProperty): boolean => {
// If there's no parent rule on then it isn't layered and is thus not in a previous one.
if (!(other.ownerStyle.parentRule instanceof CSSStyleRule)) {
return false;
}
// Element-attached style -> author origin counts as a previous layer transition for revert-layer.
if (property.ownerStyle.type === Type.Inline) {
return true;
}
// Compare layers
if (property.ownerStyle.parentRule instanceof CSSStyleRule &&
other.ownerStyle.parentRule?.origin === Protocol.CSS.StyleSheetOrigin.Regular) {
return JSON.stringify(other.ownerStyle.parentRule.layers) !==
JSON.stringify(property.ownerStyle.parentRule.layers);
}
return false;
};
switch (keyword) {
case CSSWideKeyword.INITIAL:
return this.#findCustomPropertyRegistration(property);
case CSSWideKeyword.INHERIT:
return this.#findPropertyInParentCascade(property) ?? this.#findCustomPropertyRegistration(property);
case CSSWideKeyword.REVERT:
return this.#findPropertyInPreviousStyle(
property,
other => other.ownerStyle.parentRule !== null &&
other.ownerStyle.parentRule.origin !==
(property.ownerStyle.parentRule?.origin ?? Protocol.CSS.StyleSheetOrigin.Regular)) ??
this.resolveGlobalKeyword(property, CSSWideKeyword.UNSET);
case CSSWideKeyword.REVERT_LAYER:
return this.#findPropertyInPreviousStyle(property, isPreviousLayer) ??
this.resolveGlobalKeyword(property, CSSWideKeyword.REVERT);
case CSSWideKeyword.UNSET:
return this.#findPropertyInParentCascadeIfInherited(property) ?? this.#findCustomPropertyRegistration(property);
}
}
computeCSSVariable(style: CSSStyleDeclaration, variableName: string): CSSVariableValue|null {
const nodeCascade = this.#styleToNodeCascade.get(style);
if (!nodeCascade) {
return null;
}
this.ensureInitialized();
return this.innerComputeCSSVariable(nodeCascade, variableName);
}
private innerComputeCSSVariable(nodeCascade: NodeCascade, variableName: string, sccRecord = new SCCRecord()):
CSSVariableValue|null {
const availableCSSVariables = this.#availableCSSVariables.get(nodeCascade);
const computedCSSVariables = this.#computedCSSVariables.get(nodeCascade);
if (!computedCSSVariables || !availableCSSVariables?.has(variableName)) {
return null;
}
if (computedCSSVariables?.has(variableName)) {
return computedCSSVariables.get(variableName) || null;
}
let definedValue = availableCSSVariables.get(variableName);
if (definedValue === undefined || definedValue === null) {
return null;
}
if (definedValue.declaration.declaration instanceof CSSProperty && definedValue.declaration.value &&
CSSMetadata.isCSSWideKeyword(definedValue.declaration.value)) {
const resolvedProperty =
this.resolveGlobalKeyword(definedValue.declaration.declaration, definedValue.declaration.value);
if (!resolvedProperty) {
return definedValue;
}
const declaration = new CSSValueSource(resolvedProperty);
const {value} = declaration;
if (!value) {
return definedValue;
}
definedValue = {declaration, value};
}
const ast = PropertyParser.tokenizeDeclaration(`--${variableName}`, definedValue.value);
if (!ast) {
return null;
}
// While computing CSS variable values we need to detect declaration cycles. Every declaration on the cycle is
// invalid. However, var()s outside of the cycle that reference a property on the cycle are not automatically
// invalid, but rather use the fallback value. We use a version of Tarjan's algorithm to detect cycles, which are
// SCCs on the custom property dependency graph. Computing variable values is DFS. When encountering a previously
// unseen variable, we record its discovery time. We keep a stack of visited variables and detect cycles when we
// find a reference to a variable already on the stack. For each node we also keep track of the "root" of the
// corresponding SCC, which is the node in that component with the smallest discovery time. This is determined by
// bubbling up the minimum discovery time whenever we close a cycle.
const record = sccRecord.add(nodeCascade, variableName);
const matching = PropertyParser.BottomUpTreeMatching.walk(
ast, [new PropertyParser.VariableMatcher((match: PropertyParser.VariableMatch) => {
const parentStyle = definedValue.declaration.style;
const nodeCascade = this.#styleToNodeCascade.get(parentStyle);
if (!nodeCascade) {
return null;
}
const childRecord = sccRecord.get(nodeCascade, match.name);
if (childRecord) {
if (sccRecord.isInInProgressSCC(childRecord)) {
// Cycle detected, update the root.
record.updateRoot(childRecord);
return null;
}
// We've seen the variable before, so we can look up the text directly.
return this.#computedCSSVariables.get(nodeCascade)?.get(match.name)?.value ?? null;
}
const cssVariableValue = this.innerComputeCSSVariable(nodeCascade, match.name, sccRecord);
// Variable reference is resolved, so return it.
const newChildRecord = sccRecord.get(nodeCascade, match.name);
// The SCC record for the referenced variable may not exist if the var was already computed in a previous
// iteration. That means it's in a different SCC.
newChildRecord && record.updateRoot(newChildRecord);
if (cssVariableValue?.value !== undefined) {
return cssVariableValue.value;
}
// Variable reference is not resolved, use the fallback.
if (match.fallback.length === 0 ||
match.matching.hasUnresolvedVarsRange(match.fallback[0], match.fallback[match.fallback.length - 1])) {
return null;
}
return match.matching.getComputedTextRange(match.fallback[0], match.fallback[match.fallback.length - 1]);
})]);
const decl = PropertyParser.ASTUtils.siblings(PropertyParser.ASTUtils.declValue(matching.ast.tree));
const computedText = decl.length > 0 ? matching.getComputedTextRange(decl[0], decl[decl.length - 1]) : '';
if (record.isRootEntry) {
// Variables are kept on the stack until all descendents in the same SCC have been visited. That's the case when
// completing the recursion on the root of the SCC.
const scc = sccRecord.finishSCC(record);
if (scc.length > 1) {
for (const entry of scc) {
console.assert(entry.nodeCascade === nodeCascade, 'Circles should be within the cascade');
computedCSSVariables.set(entry.name, null);
}
return null;
}
}
if (decl.length > 0 && matching.hasUnresolvedVarsRange(decl[0], decl[decl.length - 1])) {
computedCSSVariables.set(variableName, null);
return null;
}
const cssVariableValue = {value: computedText, declaration: definedValue.declaration};
computedCSSVariables.set(variableName, cssVariableValue);
return cssVariableValue;
}
styles(): CSSStyleDeclaration[] {
return Array.from(this.#styleToNodeCascade.keys());
}
propertyState(property: CSSProperty): PropertyState|null {
this.ensureInitialized();
return this.#propertiesState.get(property) || null;
}
reset(): void {
this.#initialized = f