UNPKG

chrome-devtools-frontend

Version:
1,240 lines (1,097 loc) • 53.8 kB
// 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