mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-28 11:22:57 +08:00
feat: update css formatter class to add inherited rules (#2715)
Adds support for inherited CSS rules in CssFormatter, resolving parent DOM element details and selectors for inherited declarations Co-authored-by: Samiya Caur <samiyac@chromium.org>
This commit is contained in:
co-authored by
Samiya Caur
parent
8c8616fcc4
commit
3d7e7bb438
+157
-10
@@ -99,7 +99,21 @@ export interface MatchedRule {
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export type CascadeRule = NodeStyleRule | AnimationRule | MatchedRule;
|
||||
export interface InheritedRule {
|
||||
type: 'inherited';
|
||||
node: {
|
||||
uid?: string;
|
||||
selector: string;
|
||||
};
|
||||
selector?: string;
|
||||
matchingSelectors?: string[];
|
||||
source?: string;
|
||||
ancestors?: AncestorCSSRule[];
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export type CascadeRule =
|
||||
NodeStyleRule | AnimationRule | MatchedRule | InheritedRule;
|
||||
|
||||
export interface StructuredCssStyles {
|
||||
element: {
|
||||
@@ -347,7 +361,7 @@ interface RuleMetadata {
|
||||
* Extracts common metadata (`ancestors`, `matchingSelectors`, `source`)
|
||||
* uniformly across matched rules, inherited rules, and pseudo-element rules.
|
||||
*/
|
||||
function getRuleMetadata(
|
||||
function getCSSStyleRuleMetadata(
|
||||
rule: DevTools.CSSRule.CSSStyleRule | undefined,
|
||||
matchedStyles: MatchedStyles,
|
||||
containerDetails?: Map<ContainerQuery, ResolvedContainerDetails>,
|
||||
@@ -381,6 +395,46 @@ function formatPropertyLine(prop: StructuredCssProperty): string {
|
||||
return `${stateStr}${prop.name}: ${prop.value}${imp};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters properties to only those that can be inherited from an ancestor element.
|
||||
*/
|
||||
function getInheritableProperties(
|
||||
properties: DevTools.CSSProperty.CSSProperty[],
|
||||
matchedStyles: MatchedStyles,
|
||||
): DevTools.CSSProperty.CSSProperty[] {
|
||||
return properties.filter(prop => {
|
||||
if (DevTools.CSSMetadata.cssMetadata().isCustomProperty(prop.name)) {
|
||||
const registered = matchedStyles.getRegisteredProperty?.(prop.name);
|
||||
if (registered) {
|
||||
return registered.inherits();
|
||||
}
|
||||
}
|
||||
return DevTools.CSSMetadata.cssMetadata().isPropertyInherited(prop.name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves DOM ancestor node details when a style declaration is inherited.
|
||||
*/
|
||||
function getParentNodeInfo(
|
||||
style: DevTools.CSSStyleDeclaration.CSSStyleDeclaration,
|
||||
matchedStyles: MatchedStyles,
|
||||
resolveUid?: UidResolver,
|
||||
): {uid?: string; selector: string} | undefined {
|
||||
if (!matchedStyles.isInherited?.(style)) {
|
||||
return undefined;
|
||||
}
|
||||
const parentNode = matchedStyles.nodeForStyle?.(style);
|
||||
if (!parentNode) {
|
||||
return undefined;
|
||||
}
|
||||
const parentUid = resolveUid?.(parentNode.backendNodeId());
|
||||
return {
|
||||
...(parentUid ? {uid: parentUid} : {}),
|
||||
selector: parentNode.simpleSelector(),
|
||||
};
|
||||
}
|
||||
|
||||
function getCascadeRuleHeader(rule: CascadeRule): string {
|
||||
let selector: string;
|
||||
switch (rule.type) {
|
||||
@@ -391,6 +445,9 @@ function getCascadeRuleHeader(rule: CascadeRule): string {
|
||||
case 'matched':
|
||||
selector = rule.selector;
|
||||
break;
|
||||
case 'inherited':
|
||||
selector = rule.selector ?? 'element.style';
|
||||
break;
|
||||
}
|
||||
const source = 'source' in rule ? rule.source : undefined;
|
||||
return source ? `${selector} (${source})` : selector;
|
||||
@@ -481,7 +538,15 @@ function appendCssSectionsToString(
|
||||
): void {
|
||||
for (const rule of styles.rules) {
|
||||
writer.writeEmptyLine();
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
if (rule.type === 'inherited') {
|
||||
const uidStr = rule.node.uid ? ` (uid: "${rule.node.uid}")` : '';
|
||||
writer.writeLine(`Inherited from ${rule.node.selector}${uidStr}:`);
|
||||
writer.indent();
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
writer.dedent();
|
||||
} else {
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,10 +580,23 @@ export class CssFormatter {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchedStyles.isInherited(style)) {
|
||||
const inheritedRule = CssFormatter.#createInheritedRule(
|
||||
style,
|
||||
properties,
|
||||
matchedStyles,
|
||||
options,
|
||||
);
|
||||
if (inheritedRule) {
|
||||
rules.push(inheritedRule);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (style.type === DevTools.CSSStyleDeclaration.Type.Transition) {
|
||||
rules.push({
|
||||
type: 'transition',
|
||||
selector: 'transitions style',
|
||||
selector: CssFormatter.#getNodeStyleSelector(style),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
} else if (style.type === DevTools.CSSStyleDeclaration.Type.Animation) {
|
||||
@@ -526,21 +604,19 @@ export class CssFormatter {
|
||||
rules.push({
|
||||
type: 'animation',
|
||||
...(animName ? {name: animName} : {}),
|
||||
selector: animName ? `${animName} animation` : 'animation style',
|
||||
selector: CssFormatter.#getNodeStyleSelector(style),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
} else if (style.type === DevTools.CSSStyleDeclaration.Type.Attributes) {
|
||||
const node = matchedStyles.nodeForStyle(style);
|
||||
const tag = node ? node.nodeNameInCorrectCase() : '';
|
||||
rules.push({
|
||||
type: 'attributes',
|
||||
selector: tag ? `${tag}[attributes style]` : '[attributes style]',
|
||||
selector: CssFormatter.#getNodeStyleSelector(style, matchedStyles),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
} else if (style.type === DevTools.CSSStyleDeclaration.Type.Inline) {
|
||||
rules.push({
|
||||
type: 'inline',
|
||||
selector: 'element.style',
|
||||
selector: CssFormatter.#getNodeStyleSelector(style),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
} else if (style.parentRule instanceof DevTools.CSSRule.CSSStyleRule) {
|
||||
@@ -562,7 +638,11 @@ export class CssFormatter {
|
||||
matchedStyles: MatchedStyles,
|
||||
options: CssFormatterOptions,
|
||||
): MatchedRule {
|
||||
const meta = getRuleMetadata(rule, matchedStyles, options.containerDetails);
|
||||
const meta = getCSSStyleRuleMetadata(
|
||||
rule,
|
||||
matchedStyles,
|
||||
options.containerDetails,
|
||||
);
|
||||
return {
|
||||
type: 'matched',
|
||||
selector: rule.selectorText(),
|
||||
@@ -572,6 +652,73 @@ export class CssFormatter {
|
||||
};
|
||||
}
|
||||
|
||||
static #getNodeStyleSelector(
|
||||
style: DevTools.CSSStyleDeclaration.CSSStyleDeclaration,
|
||||
matchedStyles?: MatchedStyles,
|
||||
): string {
|
||||
switch (style.type) {
|
||||
case DevTools.CSSStyleDeclaration.Type.Transition:
|
||||
return 'transitions style';
|
||||
case DevTools.CSSStyleDeclaration.Type.Animation: {
|
||||
const animName = style.animationName();
|
||||
return animName ? `${animName} animation` : 'animation style';
|
||||
}
|
||||
case DevTools.CSSStyleDeclaration.Type.Attributes: {
|
||||
const node = matchedStyles?.nodeForStyle(style);
|
||||
const tag = node ? node.nodeNameInCorrectCase() : '';
|
||||
return tag ? `${tag}[attributes style]` : '[attributes style]';
|
||||
}
|
||||
case DevTools.CSSStyleDeclaration.Type.Inline:
|
||||
return 'element.style';
|
||||
default:
|
||||
if (style.parentRule instanceof DevTools.CSSRule.CSSStyleRule) {
|
||||
return style.parentRule.selectorText();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static #createInheritedRule(
|
||||
style: DevTools.CSSStyleDeclaration.CSSStyleDeclaration,
|
||||
properties: DevTools.CSSProperty.CSSProperty[],
|
||||
matchedStyles: MatchedStyles,
|
||||
options: CssFormatterOptions,
|
||||
): InheritedRule | undefined {
|
||||
const node = getParentNodeInfo(style, matchedStyles, options.resolveUid);
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
const inheritableProps = getInheritableProperties(
|
||||
properties,
|
||||
matchedStyles,
|
||||
);
|
||||
if (!inheritableProps.length) {
|
||||
return undefined;
|
||||
}
|
||||
const rule =
|
||||
style.parentRule instanceof DevTools.CSSRule.CSSStyleRule
|
||||
? style.parentRule
|
||||
: undefined;
|
||||
const meta = getCSSStyleRuleMetadata(
|
||||
rule,
|
||||
matchedStyles,
|
||||
options.containerDetails,
|
||||
);
|
||||
const selector =
|
||||
CssFormatter.#getNodeStyleSelector(style, matchedStyles) || undefined;
|
||||
|
||||
return {
|
||||
type: 'inherited',
|
||||
node,
|
||||
...(selector ? {selector} : {}),
|
||||
...meta,
|
||||
properties: CssFormatter.#formatProperties(
|
||||
inheritableProps,
|
||||
matchedStyles,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static #formatProperties(
|
||||
props: DevTools.CSSProperty.CSSProperty[],
|
||||
matchedStyles: MatchedStyles,
|
||||
|
||||
@@ -242,6 +242,100 @@ Styles for div#main (uid: "1_1"):
|
||||
(no styles)
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inherited styles from ancestors and ignores non-inheritable ones toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "child-1",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "inherited",
|
||||
"node": {
|
||||
"selector": "section#parent-sec"
|
||||
},
|
||||
"selector": ".parent-style",
|
||||
"source": "<style>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "black",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"name": "--custom-var",
|
||||
"value": "10px",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inherited styles from ancestors and ignores non-inheritable ones toString 1`] = `
|
||||
Styles for button (uid: "child-1"):
|
||||
|
||||
Inherited from section#parent-sec:
|
||||
.parent-style (<style>) {
|
||||
color: black;
|
||||
--custom-var: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inherited transition and animation styles with parent node toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "child-elem",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "inherited",
|
||||
"node": {
|
||||
"selector": "div#wrapper"
|
||||
},
|
||||
"selector": "transitions style",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "purple",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "inherited",
|
||||
"node": {
|
||||
"selector": "div#wrapper"
|
||||
},
|
||||
"selector": "pulse animation",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "orange",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inherited transition and animation styles with parent node toString 1`] = `
|
||||
Styles for button (uid: "child-elem"):
|
||||
|
||||
Inherited from div#wrapper:
|
||||
transitions style {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
Inherited from div#wrapper:
|
||||
pulse animation {
|
||||
color: orange;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats injected stylesheet rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
|
||||
@@ -375,4 +375,49 @@ describe('CssFormatter', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
return new CssFormatter(matchedStyles, {uid: 'item-1'});
|
||||
});
|
||||
|
||||
formatterTest(
|
||||
'formats inherited styles from ancestors and ignores non-inheritable ones',
|
||||
() => {
|
||||
const inhStyle = createMockCSSStyleDeclaration(
|
||||
[
|
||||
createMockCSSProperty('color', 'black'),
|
||||
createMockCSSProperty('margin', '20px'),
|
||||
createMockCSSProperty('--custom-var', '10px'),
|
||||
],
|
||||
{rule: createMockCSSStyleRule('.parent-style')},
|
||||
);
|
||||
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
inheritedStyles: [inhStyle],
|
||||
parentNode: 'section#parent-sec',
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'child-1'});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTest(
|
||||
'formats inherited transition and animation styles with parent node',
|
||||
() => {
|
||||
const inhTransition = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'purple')],
|
||||
{type: DevTools.CSSStyleDeclaration.Type.Transition},
|
||||
);
|
||||
const inhAnimation = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'orange')],
|
||||
{
|
||||
type: DevTools.CSSStyleDeclaration.Type.Animation,
|
||||
animationName: 'pulse',
|
||||
},
|
||||
);
|
||||
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
inheritedStyles: [inhTransition, inhAnimation],
|
||||
parentNode: 'div#wrapper',
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'child-elem'});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+10
-2
@@ -378,6 +378,7 @@ export function createMockCSSStyleRule(
|
||||
export interface MockCSSMatchedStylesParams {
|
||||
node?: string | DevTools.DOMModel.DOMNode;
|
||||
nodeStyles?: DevTools.CSSStyleDeclaration.CSSStyleDeclaration[];
|
||||
inheritedStyles?: DevTools.CSSStyleDeclaration.CSSStyleDeclaration[];
|
||||
parentNode?: string | DevTools.DOMModel.DOMNode;
|
||||
nodeForStyleMap?: Map<
|
||||
DevTools.CSSStyleDeclaration.CSSStyleDeclaration,
|
||||
@@ -394,7 +395,11 @@ export function createMockCSSMatchedStyles(
|
||||
typeof params.node === 'string'
|
||||
? createMockDOMNode({selector: params.node})
|
||||
: (params.node ?? createMockDOMNode());
|
||||
const nodeStyles = params.nodeStyles ?? [];
|
||||
|
||||
const inheritedStyles = params.inheritedStyles ?? [];
|
||||
const nodeStyles = params.nodeStyles
|
||||
? [...params.nodeStyles, ...inheritedStyles]
|
||||
: inheritedStyles;
|
||||
|
||||
const defaultParentNode =
|
||||
typeof params.parentNode === 'string'
|
||||
@@ -409,11 +414,14 @@ export function createMockCSSMatchedStyles(
|
||||
);
|
||||
mock.node.returns(mockNode);
|
||||
mock.nodeStyles.returns(nodeStyles);
|
||||
mock.inheritedStyles.returns(inheritedStyles);
|
||||
|
||||
mock.nodeForStyle.callsFake(
|
||||
style => nodeForStyleMap.get(style) ?? defaultParentNode ?? null,
|
||||
);
|
||||
|
||||
mock.isInherited.callsFake(style =>
|
||||
Boolean(inheritedStyles.find(inheritedStyle => inheritedStyle === style)),
|
||||
);
|
||||
mock.propertyState.callsFake(prop => propertyStates.get(prop) ?? 'Active');
|
||||
mock.getMatchingSelectors.callsFake(
|
||||
rule => params.matchingSelectorsMap?.get(rule) ?? [],
|
||||
|
||||
Reference in New Issue
Block a user