Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion src/formatters/CssFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -169,6 +183,7 @@ export type CascadeRule =
| MatchedRule
| InheritedRule
| PseudoElementRule
| KeyframesRule
| AtRule
| PositionTryRule
| PropertyRule
Expand Down Expand Up @@ -511,6 +526,7 @@ function getCascadeRuleHeader(rule: CascadeRule): string {
case 'animation':
case 'attributes':
case 'matched':
case 'keyframes':
case 'at-rule':
case 'property':
case 'function':
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions tests/formatters/CssFormatter.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
26 changes: 26 additions & 0 deletions tests/formatters/CssFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createMockCSSPositionTryRule,
createMockCSSRegisteredProperty,
createMockCSSFunctionRule,
createMockCSSKeyframesRule,
} from '../mocks.js';

describe('CssFormatter', () => {
Expand Down Expand Up @@ -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'});
},
);
});
36 changes: 36 additions & 0 deletions tests/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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 ?? []);
Expand Down