mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-28 11:22:57 +08:00
feat: update css formatter to add keyframes rules (#2718)
Adds `@keyframes` animation rule support to CssFormatter, parsing individual animation steps and their styled properties. Co-authored-by: Samiya Caur <samiyac@chromium.org>
This commit is contained in:
co-authored by
Samiya Caur
parent
d4a0620ce6
commit
cdc365ca95
@@ -126,6 +126,20 @@ export interface PseudoElementRule {
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export interface KeyframeStep {
|
||||
keyText: string;
|
||||
source?: string;
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export interface KeyframesRule {
|
||||
type: 'keyframes';
|
||||
name: string;
|
||||
selector: string;
|
||||
source?: string;
|
||||
keyframes: KeyframeStep[];
|
||||
}
|
||||
|
||||
export interface AtRule {
|
||||
type: 'at-rule';
|
||||
atRuleType: string;
|
||||
@@ -169,6 +183,7 @@ export type CascadeRule =
|
||||
| MatchedRule
|
||||
| InheritedRule
|
||||
| PseudoElementRule
|
||||
| KeyframesRule
|
||||
| AtRule
|
||||
| PositionTryRule
|
||||
| PropertyRule
|
||||
@@ -511,6 +526,7 @@ function getCascadeRuleHeader(rule: CascadeRule): string {
|
||||
case 'animation':
|
||||
case 'attributes':
|
||||
case 'matched':
|
||||
case 'keyframes':
|
||||
case 'at-rule':
|
||||
case 'property':
|
||||
case 'function':
|
||||
@@ -579,7 +595,7 @@ function formatAncestorRuleHeader(ancestor: AncestorCSSRule): {
|
||||
|
||||
function appendRuleWithAncestors(
|
||||
writer: IndentedWriter,
|
||||
rule: CascadeRule,
|
||||
rule: Exclude<CascadeRule, KeyframesRule>,
|
||||
): void {
|
||||
const ancestors = 'ancestors' in rule ? rule.ancestors : undefined;
|
||||
let ancestorCount = 0;
|
||||
@@ -611,6 +627,26 @@ function appendRuleWithAncestors(
|
||||
}
|
||||
}
|
||||
|
||||
function appendKeyframesRule(
|
||||
writer: IndentedWriter,
|
||||
rule: KeyframesRule,
|
||||
): void {
|
||||
const header = getCascadeRuleHeader(rule);
|
||||
writer.writeLine(`${header} {`);
|
||||
writer.indent();
|
||||
for (const step of rule.keyframes) {
|
||||
writer.writeLine(`${step.keyText} {`);
|
||||
writer.indent();
|
||||
for (const prop of step.properties) {
|
||||
writer.writeLine(formatPropertyLine(prop));
|
||||
}
|
||||
writer.dedent();
|
||||
writer.writeLine('}');
|
||||
}
|
||||
writer.dedent();
|
||||
writer.writeLine('}');
|
||||
}
|
||||
|
||||
function appendCssSectionsToString(
|
||||
writer: IndentedWriter,
|
||||
styles: StructuredCssStyles,
|
||||
@@ -631,6 +667,8 @@ function appendCssSectionsToString(
|
||||
}
|
||||
writer.writeComment(`Pseudo ${rule.pseudoType} element${inheritedStr}`);
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
} else if (rule.type === 'keyframes') {
|
||||
appendKeyframesRule(writer, rule);
|
||||
} else {
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
}
|
||||
@@ -654,6 +692,7 @@ export class CssFormatter {
|
||||
const rules: CascadeRule[] = [];
|
||||
CssFormatter.#collectNodeStyles(rules, matchedStyles, options);
|
||||
CssFormatter.#collectPseudoStyles(rules, matchedStyles, options);
|
||||
CssFormatter.#collectKeyframes(rules, matchedStyles);
|
||||
CssFormatter.#collectAtRules(rules, matchedStyles);
|
||||
CssFormatter.#collectPositionTryRules(rules, matchedStyles);
|
||||
CssFormatter.#collectRegisteredProperties(rules, matchedStyles);
|
||||
@@ -887,6 +926,50 @@ export class CssFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
static #collectKeyframes(
|
||||
rules: CascadeRule[],
|
||||
matchedStyles: MatchedStyles,
|
||||
): void {
|
||||
const keyframesRules = matchedStyles.keyframes?.() ?? [];
|
||||
for (const keyframesRule of keyframesRules) {
|
||||
const name = keyframesRule.name?.()?.text ?? '';
|
||||
const rawKeyframes = keyframesRule.keyframes?.() ?? [];
|
||||
|
||||
const steps: KeyframeStep[] = [];
|
||||
let parentSource: string | undefined;
|
||||
|
||||
for (const keyframe of rawKeyframes) {
|
||||
const properties = CssFormatter.#getStyleProperties(keyframe.style);
|
||||
if (!properties.length) {
|
||||
continue;
|
||||
}
|
||||
const keyText = keyframe.key?.()?.text ?? '';
|
||||
const source = getSourceLocation(keyframe);
|
||||
if (!parentSource && source) {
|
||||
parentSource = source;
|
||||
}
|
||||
|
||||
steps.push({
|
||||
keyText,
|
||||
...(source ? {source} : {}),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
}
|
||||
|
||||
if (!steps.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rules.push({
|
||||
type: 'keyframes',
|
||||
name,
|
||||
selector: `@keyframes ${name}`,
|
||||
...(parentSource ? {source: parentSource} : {}),
|
||||
keyframes: steps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static #collectAtRules(
|
||||
rules: CascadeRule[],
|
||||
matchedStyles: MatchedStyles,
|
||||
|
||||
@@ -107,6 +107,60 @@ Styles for button (uid: "elem-func"):
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @keyframes rule with multiple steps and source location toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-kf",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "keyframes",
|
||||
"name": "slideIn",
|
||||
"selector": "@keyframes slideIn",
|
||||
"source": "animations.css:11",
|
||||
"keyframes": [
|
||||
{
|
||||
"keyText": "from",
|
||||
"source": "animations.css:11",
|
||||
"properties": [
|
||||
{
|
||||
"name": "opacity",
|
||||
"value": "0",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"keyText": "to",
|
||||
"source": "animations.css:14",
|
||||
"properties": [
|
||||
{
|
||||
"name": "opacity",
|
||||
"value": "1",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @keyframes rule with multiple steps and source location toString 1`] = `
|
||||
Styles for button (uid: "elem-kf"):
|
||||
|
||||
@keyframes slideIn (animations.css:11) {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @layer, @media, @supports, and @starting-style ancestor rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
createMockCSSPositionTryRule,
|
||||
createMockCSSRegisteredProperty,
|
||||
createMockCSSFunctionRule,
|
||||
createMockCSSKeyframesRule,
|
||||
} from '../mocks.js';
|
||||
|
||||
describe('CssFormatter', () => {
|
||||
@@ -622,4 +623,29 @@ describe('CssFormatter', () => {
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-func'});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTest(
|
||||
'formats @keyframes rule with multiple steps and source location',
|
||||
() => {
|
||||
const keyframesRule = createMockCSSKeyframesRule('slideIn', [
|
||||
{
|
||||
key: 'from',
|
||||
properties: [createMockCSSProperty('opacity', '0')],
|
||||
sourceURL: 'animations.css',
|
||||
range: {startLine: 10, startColumn: 2, endLine: 12, endColumn: 3},
|
||||
},
|
||||
{
|
||||
key: 'to',
|
||||
properties: [createMockCSSProperty('opacity', '1')],
|
||||
sourceURL: 'animations.css',
|
||||
range: {startLine: 13, startColumn: 2, endLine: 15, endColumn: 3},
|
||||
},
|
||||
]);
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
keyframes: [keyframesRule],
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-kf'});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -45,6 +45,8 @@ export type MockCSSMatchedStyles =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSMatchedStyles.CSSMatchedStyles>;
|
||||
export type MockCSSStyleRule =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSRule.CSSStyleRule>;
|
||||
export type MockCSSKeyframesRule =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSRule.CSSKeyframesRule>;
|
||||
export type MockCSSAtRule =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSRule.CSSAtRule>;
|
||||
export type MockCSSPositionTryRule =
|
||||
@@ -383,6 +385,38 @@ export function createMockCSSStyleRule(
|
||||
return rule;
|
||||
}
|
||||
|
||||
export function createMockCSSKeyframesRule(
|
||||
name: string,
|
||||
keyframes: Array<{
|
||||
key: string;
|
||||
properties: DevTools.CSSProperty.CSSProperty[];
|
||||
sourceURL?: string;
|
||||
range?: {
|
||||
startLine: number;
|
||||
startColumn: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
};
|
||||
}>,
|
||||
): MockCSSKeyframesRule {
|
||||
const rule = sinon.createStubInstance(DevTools.CSSRule.CSSKeyframesRule);
|
||||
const mockKeyframes = [];
|
||||
for (const kf of keyframes) {
|
||||
const kfMock = sinon.createStubInstance(DevTools.CSSRule.CSSKeyframeRule);
|
||||
attachRuleMeta(kfMock, kf.sourceURL);
|
||||
const style = createMockCSSStyleDeclaration(kf.properties, {
|
||||
rule: kfMock,
|
||||
range: kf.range,
|
||||
});
|
||||
kfMock.key.returns(createCSSValue(kf.key));
|
||||
Object.assign(kfMock, {style});
|
||||
mockKeyframes.push(kfMock);
|
||||
}
|
||||
rule.name.returns(createCSSValue(name));
|
||||
rule.keyframes.returns(mockKeyframes);
|
||||
return rule;
|
||||
}
|
||||
|
||||
export function createMockCSSAtRule(
|
||||
type: string,
|
||||
options: {
|
||||
@@ -524,6 +558,7 @@ export interface MockCSSMatchedStylesParams {
|
||||
node?: string | DevTools.DOMModel.DOMNode;
|
||||
nodeStyles?: DevTools.CSSStyleDeclaration.CSSStyleDeclaration[];
|
||||
inheritedStyles?: DevTools.CSSStyleDeclaration.CSSStyleDeclaration[];
|
||||
keyframes?: DevTools.CSSRule.CSSKeyframesRule[];
|
||||
atRules?: DevTools.CSSRule.CSSAtRule[];
|
||||
positionTryRules?: DevTools.CSSRule.CSSPositionTryRule[];
|
||||
registeredProperties?: DevTools.CSSMatchedStyles.CSSRegisteredProperty[];
|
||||
@@ -576,6 +611,7 @@ export function createMockCSSMatchedStyles(
|
||||
mock.node.returns(mockNode);
|
||||
mock.nodeStyles.returns(nodeStyles);
|
||||
mock.inheritedStyles.returns(inheritedStyles);
|
||||
mock.keyframes.returns(params.keyframes ?? []);
|
||||
mock.atRules.returns(params.atRules ?? []);
|
||||
mock.positionTryRules.returns(params.positionTryRules ?? []);
|
||||
mock.registeredProperties.returns(params.registeredProperties ?? []);
|
||||
|
||||
Reference in New Issue
Block a user