Skip to content

Commit 080eaef

Browse files
authored
Fix at-rules combined with plain CSS nesting (#2725)
Closes #2322
1 parent f6f73f8 commit 080eaef

File tree

8 files changed

+241
-35
lines changed

8 files changed

+241
-35
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.97.3
2+
3+
* Fix a bug where nesting an at-rule within multiple style rules in plain CSS
4+
could cause outer style rules to be omitted.
5+
16
## 1.97.2
27

38
* Additional fixes for implicit configuration when nested imports are involved.

lib/src/visitor/async_evaluate.dart

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,26 @@ final class _EvaluateVisitor
336336
/// If this is empty, that indicates that the current module is not configured.
337337
var _configuration = const Configuration.empty();
338338

339+
/// Returns whether the current position in the output stylesheet uses plain
340+
/// CSS nesting.
341+
///
342+
/// If it does, it's safe to use other features of the CSS nesting spec
343+
/// because the user has already opted into a narrower browser compatibility
344+
/// window.
345+
bool get _hasCssNesting {
346+
ModifiableCssParentNode? parent = _styleRule;
347+
while (true) {
348+
switch (parent?.parent) {
349+
case CssStyleRule _:
350+
return true;
351+
case var grandparent?:
352+
parent = grandparent;
353+
case null:
354+
return false;
355+
}
356+
}
357+
}
358+
339359
/// Creates a new visitor.
340360
///
341361
/// Most arguments are the same as those to [evaluateAsync].
@@ -1554,8 +1574,23 @@ final class _EvaluateVisitor
15541574
_inUnknownAtRule = true;
15551575
}
15561576

1577+
// If the user has already opted into plain CSS nesting, don't bother with
1578+
// any merging or bubbling; this rule is already only usable by browsers
1579+
// that support nesting natively anyway.
1580+
var rule = ModifiableCssAtRule(name, node.span, value: value);
1581+
if (_hasCssNesting) {
1582+
await _withParent(rule, () async {
1583+
for (var child in children) {
1584+
await child.accept(this);
1585+
}
1586+
}, scopeWhen: node.hasDeclarations);
1587+
_inUnknownAtRule = wasInUnknownAtRule;
1588+
_inKeyframes = wasInKeyframes;
1589+
return null;
1590+
}
1591+
15571592
await _withParent(
1558-
ModifiableCssAtRule(name, node.span, value: value),
1593+
rule,
15591594
() async {
15601595
var styleRule = _styleRule;
15611596
if (styleRule == null || _inKeyframes || name.value == 'font-face') {
@@ -2224,6 +2259,19 @@ final class _EvaluateVisitor
22242259
}
22252260

22262261
var queries = await _visitMediaQueries(node.query);
2262+
2263+
// If the user has already opted into plain CSS nesting, don't bother with
2264+
// any merging or bubbling; this rule is already only usable by browsers
2265+
// that support nesting natively anyway.
2266+
if (_hasCssNesting) {
2267+
await _withParent(ModifiableCssMediaRule(queries, node.span), () async {
2268+
for (var child in node.children) {
2269+
await child.accept(this);
2270+
}
2271+
}, scopeWhen: false);
2272+
return null;
2273+
}
2274+
22272275
var mergedQueries = _mediaQueries.andThen(
22282276
(mediaQueries) => _mergeMediaQueries(mediaQueries, queries),
22292277
);
@@ -2366,12 +2414,12 @@ final class _EvaluateVisitor
23662414
plainCss: _stylesheet.plainCss,
23672415
);
23682416

2369-
var nest = switch (_styleRule) {
2417+
var merge = switch (_styleRule) {
23702418
null => true,
23712419
CssStyleRule(fromPlainCss: true) => false,
23722420
_ => !(_stylesheet.plainCss && parsedSelector.containsParentSelector)
23732421
};
2374-
if (nest) {
2422+
if (merge) {
23752423
if (_stylesheet.plainCss) {
23762424
for (var complex in parsedSelector.components) {
23772425
if (complex.leadingCombinators
@@ -2412,7 +2460,7 @@ final class _EvaluateVisitor
24122460
}
24132461
});
24142462
},
2415-
through: nest ? (node) => node is CssStyleRule : null,
2463+
through: merge ? (node) => node is CssStyleRule : null,
24162464
scopeWhen: node.hasDeclarations,
24172465
);
24182466
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
@@ -2490,12 +2538,23 @@ final class _EvaluateVisitor
24902538
);
24912539
}
24922540

2493-
var condition = CssValue(
2494-
await _visitSupportsCondition(node.condition),
2495-
node.condition.span,
2496-
);
2541+
var rule = ModifiableCssSupportsRule(
2542+
CssValue(
2543+
await _visitSupportsCondition(node.condition),
2544+
node.condition.span,
2545+
),
2546+
node.span);
2547+
if (_hasCssNesting) {
2548+
await _withParent(rule, () async {
2549+
for (var child in node.children) {
2550+
await child.accept(this);
2551+
}
2552+
}, scopeWhen: node.hasDeclarations);
2553+
return null;
2554+
}
2555+
24972556
await _withParent(
2498-
ModifiableCssSupportsRule(condition, node.span),
2557+
rule,
24992558
() async {
25002559
if (_styleRule case var styleRule?) {
25012560
// If we're in a style rule, copy it into the supports rule so that
@@ -3988,8 +4047,23 @@ final class _EvaluateVisitor
39884047
_inUnknownAtRule = true;
39894048
}
39904049

4050+
// If the user has already opted into plain CSS nesting, don't bother with
4051+
// any merging or bubbling; this rule is already only usable by browsers
4052+
// that support nesting natively anyway.
4053+
var rule = ModifiableCssAtRule(node.name, node.span, value: node.value);
4054+
if (_hasCssNesting) {
4055+
await _withParent(rule, () async {
4056+
for (var child in node.children) {
4057+
await child.accept(this);
4058+
}
4059+
}, scopeWhen: false);
4060+
_inUnknownAtRule = wasInUnknownAtRule;
4061+
_inKeyframes = wasInKeyframes;
4062+
return;
4063+
}
4064+
39914065
await _withParent(
3992-
ModifiableCssAtRule(node.name, node.span, value: node.value),
4066+
rule,
39934067
() async {
39944068
// We don't have to check for an unknown at-rule in a style rule here,
39954069
// because the previous compilation has already bubbled the at-rule to the
@@ -4080,6 +4154,19 @@ final class _EvaluateVisitor
40804154
);
40814155
}
40824156

4157+
// If the user has already opted into plain CSS nesting, don't bother with
4158+
// any merging or bubbling; this rule is already only usable by browsers
4159+
// that support nesting natively anyway.
4160+
if (_hasCssNesting) {
4161+
await _withParent(ModifiableCssMediaRule(node.queries, node.span),
4162+
() async {
4163+
for (var child in node.children) {
4164+
await child.accept(this);
4165+
}
4166+
}, scopeWhen: false);
4167+
return;
4168+
}
4169+
40834170
var mergedQueries = _mediaQueries.andThen(
40844171
(mediaQueries) => _mergeMediaQueries(mediaQueries, node.queries),
40854172
);
@@ -4141,12 +4228,12 @@ final class _EvaluateVisitor
41414228
}
41424229

41434230
var styleRule = _styleRule;
4144-
var nest = switch (_styleRule) {
4231+
var merge = switch (_styleRule) {
41454232
null => true,
41464233
CssStyleRule(fromPlainCss: true) => false,
41474234
_ => !(node.fromPlainCss && node.selector.containsParentSelector)
41484235
};
4149-
var originalSelector = nest
4236+
var originalSelector = merge
41504237
? node.selector.nestWithin(
41514238
styleRule?.originalSelector,
41524239
implicitParent: !_atRootExcludingStyleRule,
@@ -4171,7 +4258,7 @@ final class _EvaluateVisitor
41714258
}
41724259
});
41734260
},
4174-
through: nest ? (node) => node is CssStyleRule : null,
4261+
through: merge ? (node) => node is CssStyleRule : null,
41754262
scopeWhen: false,
41764263
);
41774264
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
@@ -4198,8 +4285,18 @@ final class _EvaluateVisitor
41984285
);
41994286
}
42004287

4288+
var rule = ModifiableCssSupportsRule(node.condition, node.span);
4289+
if (_hasCssNesting) {
4290+
await _withParent(rule, () async {
4291+
for (var child in node.children) {
4292+
await child.accept(this);
4293+
}
4294+
}, scopeWhen: false);
4295+
return;
4296+
}
4297+
42014298
await _withParent(
4202-
ModifiableCssSupportsRule(node.condition, node.span),
4299+
rule,
42034300
() async {
42044301
if (_styleRule case var styleRule?) {
42054302
// If we're in a style rule, copy it into the supports rule so that
@@ -4429,7 +4526,7 @@ final class _EvaluateVisitor
44294526
return result;
44304527
}
44314528

4432-
/// If the current [_parent] is not the last child of its grandparent, makes a
4529+
/// If the current [_parent] is not the last child of its own parent, makes a
44334530
/// new childless copy of it and sets [_parent] to that.
44344531
///
44354532
/// Otherwise, leaves [_parent] as-is.

0 commit comments

Comments
 (0)