Skip to content

Commit fa5eba4

Browse files
Add support for modern CSS at-rules @layer, @scope, and @starting-style (#1549)
1 parent 4186272 commit fa5eba4

4 files changed

Lines changed: 139 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Please also have a look at our
1010

1111
### Added
1212

13+
- Add support for modern CSS at-rules: `@layer`, `@scope`, and `@starting-style` (#1549)
14+
1315
### Changed
1416

1517
### Deprecated

src/Property/AtRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ interface AtRule extends CSSListItem
1818
*
1919
* @internal since 8.5.2
2020
*/
21-
public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values/container';
21+
public const BLOCK_RULES =
22+
'media/document/supports/region-style/font-feature-values/container/layer/scope/starting-style';
2223

2324
/**
2425
* @return non-empty-string

tests/CSSList/AtRuleBlockListTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ public static function provideSyntacticallyCorrectAtRule(): array
5151
'container' => [
5252
'@container (min-width: 60rem) { .items { background: blue; } }',
5353
],
54+
'layer named' => [
55+
'@layer theme { .button { color: blue; } }',
56+
],
57+
'layer anonymous' => [
58+
'@layer { .card { padding: 1rem; } }',
59+
],
60+
'scope with selector' => [
61+
'@scope (.card) { .title { font-size: 2rem; } }',
62+
],
63+
'scope root only' => [
64+
'@scope { .content { margin: 0; } }',
65+
],
66+
'scope with limit' => [
67+
'@scope (.article-body) to (figure) { h2 { color: red; } }',
68+
],
69+
'starting-style' => [
70+
'@starting-style { .dialog { opacity: 0; transform: translateY(-10px); } }',
71+
],
5472
];
5573
}
5674

@@ -94,4 +112,110 @@ public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void
94112

95113
self::assertNotEmpty($contents, 'Failing CSS: `' . $css . '`');
96114
}
115+
116+
/**
117+
* @return array<string, array{0: string, 1: string, 2: string, 3: int}>
118+
*/
119+
public static function provideAtRuleParsingData(): array
120+
{
121+
return [
122+
'layer with named argument' => [
123+
'@layer theme { .button { color: blue; } }',
124+
'layer',
125+
'theme',
126+
1,
127+
],
128+
'layer without arguments' => [
129+
'@layer { .card { padding: 1rem; } }',
130+
'layer',
131+
'',
132+
1,
133+
],
134+
'scope with selector' => [
135+
'@scope (.card) { .title { font-size: 2rem; } }',
136+
'scope',
137+
'(.card)',
138+
1,
139+
],
140+
'scope without selector' => [
141+
'@scope { .content { margin: 0; } }',
142+
'scope',
143+
'',
144+
1,
145+
],
146+
'scope with limit' => [
147+
'@scope (.article-body) to (figure) { h2 { color: red; } }',
148+
'scope',
149+
'(.article-body) to (figure)',
150+
1,
151+
],
152+
'starting-style' => [
153+
'@starting-style { .dialog { opacity: 0; transform: translateY(-10px); } }',
154+
'starting-style',
155+
'',
156+
1,
157+
],
158+
];
159+
}
160+
161+
/**
162+
* @return array<string, array{0: string, 1: list<string>}>
163+
*/
164+
public static function provideAtRuleRenderingData(): array
165+
{
166+
return [
167+
'layer with named argument' => [
168+
'@layer theme { .button { color: blue; } }',
169+
['@layer theme', '.button', 'color: blue'],
170+
],
171+
'scope with selector' => [
172+
'@scope (.card) { .title { font-size: 2rem; } }',
173+
['@scope (.card)', '.title', 'font-size: 2rem'],
174+
],
175+
'scope with limit' => [
176+
'@scope (.article-body) to (figure) { h2 { color: red; } }',
177+
['@scope (.article-body) to (figure)', 'h2', 'color: red'],
178+
],
179+
'starting-style' => [
180+
'@starting-style { .dialog { opacity: 0; } }',
181+
['@starting-style', '.dialog', 'opacity: 0'],
182+
],
183+
];
184+
}
185+
186+
/**
187+
* @test
188+
*
189+
* @dataProvider provideAtRuleParsingData
190+
*/
191+
public function parsesAtRuleBlockList(
192+
string $css,
193+
string $expectedName,
194+
string $expectedArgs,
195+
int $expectedContentCount
196+
): void {
197+
$contents = (new Parser($css))->parse()->getContents();
198+
$atRuleBlockList = $contents[0];
199+
200+
self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList);
201+
self::assertSame($expectedName, $atRuleBlockList->atRuleName());
202+
self::assertSame($expectedArgs, $atRuleBlockList->atRuleArgs());
203+
self::assertCount($expectedContentCount, $atRuleBlockList->getContents());
204+
}
205+
206+
/**
207+
* @test
208+
*
209+
* @dataProvider provideAtRuleRenderingData
210+
*
211+
* @param list<string> $expectedSubstrings
212+
*/
213+
public function rendersAtRuleBlockListCorrectly(string $css, array $expectedSubstrings): void
214+
{
215+
$rendered = (new Parser($css))->parse()->render();
216+
217+
foreach ($expectedSubstrings as $expected) {
218+
self::assertStringContainsString($expected, $rendered);
219+
}
220+
}
97221
}

tests/Unit/Property/AtRuleTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ final class AtRuleTest extends TestCase
1818
public function blockRulesConstantIsCorrect(): void
1919
{
2020
self::assertEqualsCanonicalizing(
21-
['media', 'document', 'supports', 'region-style', 'font-feature-values', 'container'],
21+
[
22+
'media',
23+
'document',
24+
'supports',
25+
'region-style',
26+
'font-feature-values',
27+
'container',
28+
'layer',
29+
'scope',
30+
'starting-style',
31+
],
2232
explode('/', AtRule::BLOCK_RULES)
2333
);
2434
}

0 commit comments

Comments
 (0)