From 47d4f89453f33ac5e83e541d1cc2b454191be786 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 19 Jul 2023 01:18:17 -0400 Subject: [PATCH] css: nesting transform now avoids `:is` (#1945) --- CHANGELOG.md | 67 +++++++ internal/bundler_tests/bundler_css_test.go | 36 ++-- .../bundler_tests/snapshots/snapshots_css.txt | 12 ++ internal/css_parser/css_nesting.go | 126 +++++++++++- internal/css_parser/css_parser.go | 12 -- internal/css_parser/css_parser_selector.go | 6 +- internal/css_parser/css_parser_test.go | 179 ++++++++++-------- 7 files changed, 315 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd672a95b56..df68bf96d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,73 @@ ## Unreleased +* Implement CSS nesting without `:is()` when possible ([#1945](https://github.com/evanw/esbuild/issues/1945)) + + Previously esbuild would always produce a warning when transforming nested CSS for a browser that doesn't support the `:is()` pseudo-class. This was because the nesting transform needs to generate an `:is()` in some complex cases which means the transformed CSS would then not work in that browser. However, the CSS nesting transform can often be done without generating an `:is()`. So with this release, esbuild will no longer warn when targeting browsers that don't support `:is()` in the cases where an `:is()` isn't needed to represent the nested CSS. + + In addition, esbuild's nested CSS transform has been updated to avoid generating an `:is()` in cases where an `:is()` is preferable but there's a longer alternative that is also equivalent. This update means esbuild can now generate a combinatorial explosion of CSS for complex CSS nesting syntax when targeting browsers that don't support `:is()`. This combinatorial explosion is necessary to accurately represent the original semantics. For example: + + ```css + /* Original code */ + .first, + .second, + .third { + & > & { + color: red; + } + } + + /* Old output (with --target=chrome80) */ + :is(.first, .second, .third) > :is(.first, .second, .third) { + color: red; + } + + /* New output (with --target=chrome80) */ + .first > .first, + .first > .second, + .first > .third, + .second > .first, + .second > .second, + .second > .third, + .third > .first, + .third > .second, + .third > .third { + color: red; + } + ``` + + This change means you can now use CSS nesting with esbuild when targeting an older browser that doesn't support `:is()`. You'll now only get a warning from esbuild if you use complex CSS nesting syntax that esbuild can't represent in that older browser without using `:is()`. There are two such cases: + + ```css + /* Case 1 */ + a b { + .foo & { + color: red; + } + } + + /* Case 2 */ + a { + > b& { + color: red; + } + } + ``` + + These two cases still need to use `:is()`, both for different reasons, and cannot be used when targeting an older browser that doesn't support `:is()`: + + ```css + /* Case 1 */ + .foo :is(a b) { + color: red; + } + + /* Case 2 */ + a > a:is(b) { + color: red; + } + ``` + * Automatically lower `inset` in CSS for older browsers With this release, esbuild will now automatically expand the `inset` property to the `top`, `right`, `bottom`, and `left` properties when esbuild's `target` is set to a browser that doesn't support `inset`: diff --git a/internal/bundler_tests/bundler_css_test.go b/internal/bundler_tests/bundler_css_test.go index c6fbf45ced2..db71ab1230a 100644 --- a/internal/bundler_tests/bundler_css_test.go +++ b/internal/bundler_tests/bundler_css_test.go @@ -925,6 +925,10 @@ func TestCSSExternalQueryAndHashMatchIssue1822(t *testing.T) { func TestCSSNestingOldBrowser(t *testing.T) { css_suite.expectBundled(t, bundled{ files: map[string]string{ + // These are now the only two cases that warn about ":is" not being supported + "/two-type-selectors.css": `a { .c b& { color: red; } }`, + "/two-parent-selectors.css": `a b { .c & { color: red; } }`, + "/nested-@layer.css": `a { @layer base { color: red; } }`, "/nested-@media.css": `a { @media screen { color: red; } }`, "/nested-ampersand-twice.css": `a { &, & { color: red; } }`, @@ -963,6 +967,9 @@ func TestCSSNestingOldBrowser(t *testing.T) { "/page-no-warning.css": `@page { @top-left { background: red } }`, }, entryPaths: []string{ + "/two-type-selectors.css", + "/two-parent-selectors.css", + "/nested-@layer.css", "/nested-@media.css", "/nested-ampersand-twice.css", @@ -1005,31 +1012,10 @@ func TestCSSNestingOldBrowser(t *testing.T) { UnsupportedCSSFeatures: compat.Nesting | compat.IsPseudoClass, OriginalTargetEnv: "chrome10", }, - expectedScanLog: `media-ampersand-first.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-ampersand-second.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -media-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-@layer.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-@media.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-ampersand-first.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-attribute.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-colon.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-dot.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-hash.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -nested-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-ampersand-first.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-ampersand-second.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) -toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10) + expectedScanLog: `two-parent-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10) +NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class. +two-type-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10) +NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class. `, }) } diff --git a/internal/bundler_tests/snapshots/snapshots_css.txt b/internal/bundler_tests/snapshots/snapshots_css.txt index 9da686e57b9..67d01e83ea8 100644 --- a/internal/bundler_tests/snapshots/snapshots_css.txt +++ b/internal/bundler_tests/snapshots/snapshots_css.txt @@ -180,6 +180,18 @@ console.log(void 0); ================================================================================ TestCSSNestingOldBrowser +---------- /out/two-type-selectors.css ---------- +/* two-type-selectors.css */ +.c a:is(b) { + color: red; +} + +---------- /out/two-parent-selectors.css ---------- +/* two-parent-selectors.css */ +.c :is(a b) { + color: red; +} + ---------- /out/nested-@layer.css ---------- /* nested-@layer.css */ @layer base { diff --git a/internal/css_parser/css_nesting.go b/internal/css_parser/css_nesting.go index 43b87b16e80..528f04bf428 100644 --- a/internal/css_parser/css_nesting.go +++ b/internal/css_parser/css_nesting.go @@ -1,7 +1,10 @@ package css_parser import ( + "fmt" + "github.com/evanw/esbuild/internal/ast" + "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/css_ast" "github.com/evanw/esbuild/internal/logger" ) @@ -153,6 +156,12 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower } } + // Avoid generating ":is" if it's not supported + if p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) && len(r.Selectors) > 1 { + canUseGroupDescendantCombinator = false + canUseGroupSubSelector = false + } + // Try to apply simplifications for shorter output if canUseGroupDescendantCombinator { // "& a, & b {}" => "& :is(a, b) {}" @@ -179,14 +188,104 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower } // Pass 2: Substitue "&" for the parent selector - for i := range r.Selectors { - complex := &r.Selectors[i] - results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) - parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectors) - for _, compound := range complex.Selectors { - results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator) + if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectors) <= 1 { + // If we can use ":is", or we don't have to because there's only one + // parent selector, or we are using ":is()" to match zero parent selectors + // (even if ":is" is unsupported), then substituting "&" for the parent + // selector is easy. + for i := range r.Selectors { + complex := &r.Selectors[i] + results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) + parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectors) + for _, compound := range complex.Selectors { + results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator) + } + complex.Selectors = results + } + } else { + // Otherwise if we can't use ":is", the transform is more complicated. + // Avoiding ":is" can lead to a combinatorial explosion of cases so we + // want to avoid this if possible. For example: + // + // .first, .second, .third { + // & > & { + // color: red; + // } + // } + // + // If we can use ":is" (the easy case above) then we can do this: + // + // :is(.first, .second, .third) > :is(.first, .second, .third) { + // color: red; + // } + // + // But if we can't use ":is" then we have to do this instead: + // + // .first > .first, + // .first > .second, + // .first > .third, + // .second > .first, + // .second > .second, + // .second > .third, + // .third > .first, + // .third > .second, + // .third > .third { + // color: red; + // } + // + // That combinatorial explosion is what the loop below implements. Note + // that PostCSS's implementation of nesting gets this wrong. It generates + // this instead: + // + // .first > .first, + // .second > .second, + // .third > .third { + // color: red; + // } + // + // That's not equivalent, so that's an incorrect transformation. + var selectors []css_ast.ComplexSelector + var indices []int + for { + // Every time we encounter another "&", add another dimension + offset := 0 + parent := func(loc logger.Loc) css_ast.ComplexSelector { + if offset == len(indices) { + indices = append(indices, 0) + } + index := indices[offset] + offset++ + return context.parentSelectors[index] + } + + // Do the substitution for this particular combination + for i := range r.Selectors { + complex := r.Selectors[i] + results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) + for _, compound := range complex.Selectors { + results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator) + } + complex.Selectors = results + selectors = append(selectors, complex) + offset = 0 + } + + // Do addition with carry on the indices across dimensions + carry := len(indices) + for carry > 0 { + index := &indices[carry-1] + if *index+1 < len(context.parentSelectors) { + *index++ + break + } + *index = 0 + carry-- + } + if carry == 0 { + break + } } - complex.Selectors = results + r.Selectors = selectors } // Lower all child rules using our newly substituted selector @@ -275,6 +374,7 @@ func (p *parser) substituteAmpersandsInCompoundSelector( } else { // ".foo .bar { :hover & {} }" => ":hover :is(.foo .bar) {}" // ".foo .bar { > &:hover {} }" => ".foo .bar > :is(.foo .bar):hover {}" + p.reportNestingWithGeneratedPseudoClassIs(logger.Range{Loc: nestingSelectorLoc, Len: 1}) single = css_ast.CompoundSelector{ SubclassSelectors: []css_ast.SubclassSelector{{ Loc: nestingSelectorLoc, @@ -291,6 +391,7 @@ func (p *parser) substituteAmpersandsInCompoundSelector( // Insert the type selector if single.TypeSelector != nil { if sel.TypeSelector != nil { + p.reportNestingWithGeneratedPseudoClassIs(logger.Range{Loc: nestingSelectorLoc, Len: 1}) subclassSelectorPrefix = append(subclassSelectorPrefix, css_ast.SubclassSelector{ Loc: sel.TypeSelector.FirstLoc(), Data: &css_ast.SSPseudoClassWithSelectorList{ @@ -363,3 +464,14 @@ func (p *parser) multipleComplexSelectorsToSingleComplexSelector(selectors []css } } } + +func (p *parser) reportNestingWithGeneratedPseudoClassIs(r logger.Range) { + if p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) { + text := "Transforming this CSS nesting syntax is not supported in the configured target environment" + if p.options.originalTargetEnv != "" { + text = fmt.Sprintf("%s (%s)", text, p.options.originalTargetEnv) + } + p.log.AddIDWithNotes(logger.MsgID_CSS_UnsupportedCSSNesting, logger.Warning, &p.tracker, r, text, []logger.MsgData{{ + Text: "The nesting transform for this case must generate an \":is(...)\" but the configured target environment does not support the \":is\" pseudo-class."}}) + } +} diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index d8951eaa592..09e3452ac27 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -506,7 +506,6 @@ func (p *parser) parseListOfDeclarations(opts listOfDeclarationsOpts) (list []cs case css_lexer.TAtKeyword: if p.inSelectorSubtree > 0 { p.shouldLowerNesting = true - p.reportUseOfNesting(p.current().Range, false) } list = append(list, p.parseAtRule(atRuleContext{ isDeclarationList: true, @@ -525,7 +524,6 @@ func (p *parser) parseListOfDeclarations(opts listOfDeclarationsOpts) (list []cs css_lexer.TDelimGreaterThan, css_lexer.TDelimTilde: p.shouldLowerNesting = true - p.reportUseOfNesting(p.current().Range, false) list = append(list, p.parseSelectorRuleFrom(p.index, false, parseSelectorOpts{isDeclarationContext: true})) foundNesting = true @@ -1475,16 +1473,6 @@ func (p *parser) expectValidLayerNameIdent() (string, bool) { return text, true } -func (p *parser) reportUseOfNesting(r logger.Range, didWarnAlready bool) { - if p.options.unsupportedCSSFeatures.Has(compat.Nesting) && p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) && !didWarnAlready { - text := "CSS nesting syntax is not supported in the configured target environment" - if p.options.originalTargetEnv != "" { - text = fmt.Sprintf("%s (%s)", text, p.options.originalTargetEnv) - } - p.log.AddID(logger.MsgID_CSS_UnsupportedCSSNesting, logger.Warning, &p.tracker, r, text) - } -} - func (p *parser) convertTokens(tokens []css_lexer.Token) []css_ast.Token { result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, convertTokensOpts{}) return result diff --git a/internal/css_parser/css_parser_selector.go b/internal/css_parser/css_parser_selector.go index 220dd3af15b..b8f49159426 100644 --- a/internal/css_parser/css_parser_selector.go +++ b/internal/css_parser/css_parser_selector.go @@ -258,11 +258,9 @@ type parseComplexSelectorOpts struct { func (p *parser) parseComplexSelector(opts parseComplexSelectorOpts) (result css_ast.ComplexSelector, ok bool) { // This is an extension: https://drafts.csswg.org/css-nesting-1/ - r := p.current().Range combinator := p.parseCombinator() if combinator.Byte != 0 { p.shouldLowerNesting = true - p.reportUseOfNesting(r, opts.isDeclarationContext) p.eat(css_lexer.TWhitespace) } @@ -324,7 +322,6 @@ func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_a hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand) if hasLeadingNestingSelector { p.shouldLowerNesting = true - p.reportUseOfNesting(p.current().Range, opts.isDeclarationContext) sel.NestingSelectorLoc = ast.MakeIndex32(uint32(startLoc.Start)) p.advance() } @@ -439,7 +436,6 @@ subclassSelectors: case css_lexer.TDelimAmpersand: // This is an extension: https://drafts.csswg.org/css-nesting-1/ p.shouldLowerNesting = true - p.reportUseOfNesting(subclassToken.Range, sel.HasNestingSelector()) sel.NestingSelectorLoc = ast.MakeIndex32(uint32(subclassToken.Range.Loc.Start)) p.advance() @@ -468,7 +464,7 @@ subclassSelectors: suggestion := p.source.TextForRange(r) if opts.isFirst { suggestion = fmt.Sprintf(":is(%s)", suggestion) - howToFix = "You can wrap this selector in \":is()\" as a workaround. " + howToFix = "You can wrap this selector in \":is(...)\" as a workaround. " } else { r = logger.Range{Loc: startLoc, Len: r.End() - startLoc.Start} suggestion += "&" diff --git a/internal/css_parser/css_parser_test.go b/internal/css_parser/css_parser_test.go index 9c999d0ec99..ca2311a4bdf 100644 --- a/internal/css_parser/css_parser_test.go +++ b/internal/css_parser/css_parser_test.go @@ -73,6 +73,13 @@ func expectPrintedLower(t *testing.T, contents string, expected string) { }) } +func expectPrintedLowerUnsupported(t *testing.T, unsupportedCSSFeatures compat.CSSFeature, contents string, expected string) { + t.Helper() + expectPrintedCommon(t, contents+" [lower]", contents, expected, nil, config.Options{ + UnsupportedCSSFeatures: unsupportedCSSFeatures, + }) +} + func expectPrintedWithAllPrefixes(t *testing.T, contents string, expected string) { t.Helper() expectPrintedCommon(t, contents+" [prefixed]", contents, expected, nil, config.Options{ @@ -891,72 +898,96 @@ func TestNestedSelector(t *testing.T) { expectPrintedMangle(t, "div, span::pseudo { @media screen { & { color: red } } }", "div,\nspan::pseudo {\n @media screen {\n & {\n color: red;\n }\n }\n}\n") // Lowering tests for nesting - expectPrintedLower(t, ".foo { .bar { color: red } }", ".foo .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".foo { &.bar { color: red } }", ".foo.bar {\n color: red;\n}\n") - expectPrintedLower(t, ".foo { & .bar { color: red } }", ".foo .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { .baz { color: red } }", ".foo .bar .baz {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { &.baz { color: red } }", ".foo .bar.baz {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { & .baz { color: red } }", ".foo .bar .baz {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { & > .baz { color: red } }", ".foo .bar > .baz {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { .baz & { color: red } }", ".baz :is(.foo .bar) {\n color: red;\n}\n") // NOT the same as ".baz .foo .bar" - expectPrintedLower(t, ".foo .bar { & .baz & { color: red } }", ".foo .bar .baz :is(.foo .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".foo, .bar { .baz & { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".foo, [bar~='abc'] { .baz { color: red } }", ":is(.foo, [bar~=abc]) .baz {\n color: red;\n}\n") - expectPrintedLower(t, ".foo, [bar~='a b c'] { .baz { color: red } }", ":is(.foo, [bar~=\"a b c\"]) .baz {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { .foo, .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { .foo, & .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { & .foo, .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { & .foo, & .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { .foo, &.bar { color: red } }", ".baz .foo,\n.baz.bar {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { &.foo, .bar { color: red } }", ".baz.foo,\n.baz .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".baz { &.foo, &.bar { color: red } }", ".baz:is(.foo, .bar) {\n color: red;\n}\n") - expectPrintedLower(t, ".foo { color: blue; & .bar { color: red } }", ".foo {\n color: blue;\n}\n.foo .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".foo { & .bar { color: red } color: blue }", ".foo {\n color: blue;\n}\n.foo .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".foo { color: blue; & .bar { color: red } zoom: 2 }", ".foo {\n color: blue;\n zoom: 2;\n}\n.foo .bar {\n color: red;\n}\n") - expectPrintedLower(t, ".a, .b { .c, .d { color: red } }", ":is(.a, .b) :is(.c, .d) {\n color: red;\n}\n") - expectPrintedLower(t, ".a { color: red; > .b { color: green; > .c { color: blue } } }", ".a {\n color: red;\n}\n.a > .b {\n color: green;\n}\n.a > .b > .c {\n color: blue;\n}\n") - expectPrintedLower(t, "> .a { color: red; > .b { color: green; > .c { color: blue } } }", "> .a {\n color: red;\n}\n> .a > .b {\n color: green;\n}\n> .a > .b > .c {\n color: blue;\n}\n") - expectPrintedLower(t, ".foo, .bar, .foo:before, .bar:after { &:hover { color: red } }", ":is(.foo, .bar):hover {\n color: red;\n}\n") - expectPrintedLower(t, ".foo, .bar:before { &:hover { color: red } }", ".foo:hover {\n color: red;\n}\n") - expectPrintedLower(t, ".foo, .bar:before { :hover & { color: red } }", ":hover .foo {\n color: red;\n}\n") - expectPrintedLower(t, ".bar:before { &:hover { color: red } }", ":is():hover {\n color: red;\n}\n") - expectPrintedLower(t, ".bar:before { :hover & { color: red } }", ":hover :is() {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(&.foo) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(&.foo) { color: red } }", ":where(div.foo) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(.foo&) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(.foo&) { color: red } }", ":where(div.foo) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where([href]&) { color: red } }", ":where(.xy[href]) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where([href]&) { color: red } }", ":where(div[href]) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(:hover&) { color: red } }", ":where(.xy:hover) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(:hover&) { color: red } }", ":where(div:hover) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(:is(.foo)&) { color: red } }", ":where(.xy:is(.foo)) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(:is(.foo)&) { color: red } }", ":where(div:is(.foo)) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(.foo + &) { color: red } }", ":where(.foo + .xy) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(.foo + &) { color: red } }", ":where(.foo + div) {\n color: red;\n}\n") - expectPrintedLower(t, ".xy { :where(&, span:is(.foo &)) { color: red } }", ":where(.xy, span:is(.foo .xy)) {\n color: red;\n}\n") - expectPrintedLower(t, "div { :where(&, span:is(.foo &)) { color: red } }", ":where(div, span:is(.foo div)) {\n color: red;\n}\n") - expectPrintedLower(t, "&, a { color: red }", ":scope,\na {\n color: red;\n}\n") - expectPrintedLower(t, "&, a { .b { color: red } }", ":is(:scope, a) .b {\n color: red;\n}\n") - expectPrintedLower(t, "&, a { .b { .c { color: red } } }", ":is(:scope, a) .b .c {\n color: red;\n}\n") - expectPrintedLower(t, "a { > b, > c { color: red } }", "a > :is(b, c) {\n color: red;\n}\n") - expectPrintedLower(t, "a { > b, + c { color: red } }", "a > b,\na + c {\n color: red;\n}\n") - expectPrintedLower(t, "a { & > b, & > c { color: red } }", "a > :is(b, c) {\n color: red;\n}\n") - expectPrintedLower(t, "a { & > b, & + c { color: red } }", "a > b,\na + c {\n color: red;\n}\n") - expectPrintedLower(t, "a { > b&, > c& { color: red } }", "a > :is(a:is(b), a:is(c)) {\n color: red;\n}\n") - expectPrintedLower(t, "a { > b&, + c& { color: red } }", "a > a:is(b),\na + a:is(c) {\n color: red;\n}\n") - expectPrintedLower(t, "a { > &.b, > &.c { color: red } }", "a > :is(a.b, a.c) {\n color: red;\n}\n") - expectPrintedLower(t, "a { > &.b, + &.c { color: red } }", "a > a.b,\na + a.c {\n color: red;\n}\n") - expectPrintedLower(t, ".a { > b&, > c& { color: red } }", ".a > :is(b.a, c.a) {\n color: red;\n}\n") - expectPrintedLower(t, ".a { > b&, + c& { color: red } }", ".a > b.a,\n.a + c.a {\n color: red;\n}\n") - expectPrintedLower(t, ".a { > &.b, > &.c { color: red } }", ".a > :is(.a.b, .a.c) {\n color: red;\n}\n") - expectPrintedLower(t, ".a { > &.b, + &.c { color: red } }", ".a > .a.b,\n.a + .a.c {\n color: red;\n}\n") - expectPrintedLower(t, "~ .a { > &.b, > &.c { color: red } }", "~ .a > :is(.a.b, .a.c) {\n color: red;\n}\n") - expectPrintedLower(t, "~ .a { > &.b, + &.c { color: red } }", "~ .a > .a.b,\n~ .a + .a.c {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { > &.a, > &.b { color: red } }", ".foo .bar > :is(.foo .bar.a, .foo .bar.b) {\n color: red;\n}\n") - expectPrintedLower(t, ".foo .bar { > &.a, + &.b { color: red } }", ".foo .bar > :is(.foo .bar).a,\n.foo .bar + :is(.foo .bar).b {\n color: red;\n}\n") - expectPrintedLower(t, ".demo { .lg { &.triangle, &.circle { color: red } } }", ".demo .lg:is(.triangle, .circle) {\n color: red;\n}\n") - expectPrintedLower(t, ".demo { .lg { .triangle, .circle { color: red } } }", ".demo .lg :is(.triangle, .circle) {\n color: red;\n}\n") - expectPrintedLower(t, ".card { .featured & & & { color: red } }", ".featured .card .card .card {\n color: red;\n}\n") + nesting := compat.Nesting + everything := ^compat.CSSFeature(0) + expectPrintedLowerUnsupported(t, nesting, ".foo { .bar { color: red } }", ".foo .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo { &.bar { color: red } }", ".foo.bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo { & .bar { color: red } }", ".foo .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { .baz { color: red } }", ".foo .bar .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { &.baz { color: red } }", ".foo .bar.baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { & .baz { color: red } }", ".foo .bar .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { & > .baz { color: red } }", ".foo .bar > .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { .baz & { color: red } }", ".baz :is(.foo .bar) {\n color: red;\n}\n") // NOT the same as ".baz .foo .bar" + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { & .baz & { color: red } }", ".foo .bar .baz :is(.foo .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, .bar { .baz & { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".foo, .bar { .baz & { color: red } }", ".baz .foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, [bar~='abc'] { .baz { color: red } }", ":is(.foo, [bar~=abc]) .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".foo, [bar~='abc'] { .baz { color: red } }", ".foo .baz,\n[bar~=abc] .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, [bar~='a b c'] { .baz { color: red } }", ":is(.foo, [bar~=\"a b c\"]) .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".foo, [bar~='a b c'] { .baz { color: red } }", ".foo .baz,\n[bar~=\"a b c\"] .baz {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { .foo, .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { .foo, .bar { color: red } }", ".baz .foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { .foo, & .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { .foo, & .bar { color: red } }", ".baz .foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { & .foo, .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { & .foo, .bar { color: red } }", ".baz .foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { & .foo, & .bar { color: red } }", ".baz :is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { & .foo, & .bar { color: red } }", ".baz .foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { .foo, &.bar { color: red } }", ".baz .foo,\n.baz.bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { &.foo, .bar { color: red } }", ".baz.foo,\n.baz .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".baz { &.foo, &.bar { color: red } }", ".baz:is(.foo, .bar) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".baz { &.foo, &.bar { color: red } }", ".baz.foo,\n.baz.bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo { color: blue; & .bar { color: red } }", ".foo {\n color: blue;\n}\n.foo .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo { & .bar { color: red } color: blue }", ".foo {\n color: blue;\n}\n.foo .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo { color: blue; & .bar { color: red } zoom: 2 }", ".foo {\n color: blue;\n zoom: 2;\n}\n.foo .bar {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a, .b { .c, .d { color: red } }", ":is(.a, .b) :is(.c, .d) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".a, .b { .c, .d { color: red } }", ".a .c,\n.a .d,\n.b .c,\n.b .d {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a, .b { & > & { color: red } }", ":is(.a, .b) > :is(.a, .b) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".a, .b { & > & { color: red } }", ".a > .a,\n.a > .b,\n.b > .a,\n.b > .b {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a { color: red; > .b { color: green; > .c { color: blue } } }", ".a {\n color: red;\n}\n.a > .b {\n color: green;\n}\n.a > .b > .c {\n color: blue;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "> .a { color: red; > .b { color: green; > .c { color: blue } } }", "> .a {\n color: red;\n}\n> .a > .b {\n color: green;\n}\n> .a > .b > .c {\n color: blue;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, .bar, .foo:before, .bar:after { &:hover { color: red } }", ":is(.foo, .bar):hover {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".foo, .bar, .foo:before, .bar:after { &:hover { color: red } }", ".foo:hover,\n.bar:hover {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, .bar:before { &:hover { color: red } }", ".foo:hover {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo, .bar:before { :hover & { color: red } }", ":hover .foo {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".bar:before { &:hover { color: red } }", ":is():hover {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".bar:before { :hover & { color: red } }", ":hover :is() {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(&.foo) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(&.foo) { color: red } }", ":where(div.foo) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(.foo&) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(.foo&) { color: red } }", ":where(div.foo) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where([href]&) { color: red } }", ":where(.xy[href]) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where([href]&) { color: red } }", ":where(div[href]) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(:hover&) { color: red } }", ":where(.xy:hover) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(:hover&) { color: red } }", ":where(div:hover) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(:is(.foo)&) { color: red } }", ":where(.xy:is(.foo)) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(:is(.foo)&) { color: red } }", ":where(div:is(.foo)) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(.foo + &) { color: red } }", ":where(.foo + .xy) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(.foo + &) { color: red } }", ":where(.foo + div) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".xy { :where(&, span:is(.foo &)) { color: red } }", ":where(.xy, span:is(.foo .xy)) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "div { :where(&, span:is(.foo &)) { color: red } }", ":where(div, span:is(.foo div)) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "&, a { color: red }", ":scope,\na {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "&, a { .b { color: red } }", ":is(:scope, a) .b {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "&, a { .b { color: red } }", ":scope .b,\na .b {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "&, a { .b { .c { color: red } } }", ":is(:scope, a) .b .c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "&, a { .b { .c { color: red } } }", ":scope .b .c,\na .b .c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > b, > c { color: red } }", "a > :is(b, c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "a { > b, > c { color: red } }", "a > b,\na > c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > b, + c { color: red } }", "a > b,\na + c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { & > b, & > c { color: red } }", "a > :is(b, c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "a { & > b, & > c { color: red } }", "a > b,\na > c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { & > b, & + c { color: red } }", "a > b,\na + c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > b&, > c& { color: red } }", "a > :is(a:is(b), a:is(c)) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "a { > b&, > c& { color: red } }", "a > a:is(b),\na > a:is(c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > b&, + c& { color: red } }", "a > a:is(b),\na + a:is(c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > &.b, > &.c { color: red } }", "a > :is(a.b, a.c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "a { > &.b, > &.c { color: red } }", "a > a.b,\na > a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "a { > &.b, + &.c { color: red } }", "a > a.b,\na + a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a { > b&, > c& { color: red } }", ".a > :is(b.a, c.a) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".a { > b&, > c& { color: red } }", ".a > b.a,\n.a > c.a {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a { > b&, + c& { color: red } }", ".a > b.a,\n.a + c.a {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a { > &.b, > &.c { color: red } }", ".a > :is(.a.b, .a.c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".a { > &.b, > &.c { color: red } }", ".a > .a.b,\n.a > .a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".a { > &.b, + &.c { color: red } }", ".a > .a.b,\n.a + .a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "~ .a { > &.b, > &.c { color: red } }", "~ .a > :is(.a.b, .a.c) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, "~ .a { > &.b, > &.c { color: red } }", "~ .a > .a.b,\n~ .a > .a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, "~ .a { > &.b, + &.c { color: red } }", "~ .a > .a.b,\n~ .a + .a.c {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { > &.a, > &.b { color: red } }", ".foo .bar > :is(.foo .bar.a, .foo .bar.b) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, everything, ".foo .bar { > &.a, > &.b { color: red } }", ".foo .bar > :is(.foo .bar).a,\n.foo .bar > :is(.foo .bar).b {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".foo .bar { > &.a, + &.b { color: red } }", ".foo .bar > :is(.foo .bar).a,\n.foo .bar + :is(.foo .bar).b {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".demo { .lg { &.triangle, &.circle { color: red } } }", ".demo .lg:is(.triangle, .circle) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".demo { .lg { .triangle, .circle { color: red } } }", ".demo .lg :is(.triangle, .circle) {\n color: red;\n}\n") + expectPrintedLowerUnsupported(t, nesting, ".card { .featured & & & { color: red } }", ".featured .card .card .card {\n color: red;\n}\n") // These are invalid SASS-style nested suffixes sassWarningStart := "NOTE: CSS nesting syntax does not allow the \"&\" selector to come before a type selector. " @@ -969,7 +1000,7 @@ func TestNestedSelector(t *testing.T) { expectPrintedLower(t, ".card { .nav &__header { color: red } }", ".card {\n .nav &__header {\n color: red;\n }\n}\n") expectParseError(t, ".card { &__header { color: red } }", ".card {\n &__header {\n color: red;\n }\n}\n", ": WARNING: Cannot use type selector \"__header\" directly after nesting selector \"&\"\n"+ - sassWarningStart+"You can wrap this selector in \":is()\" as a workaround. "+sassWarningEnd) + sassWarningStart+"You can wrap this selector in \":is(...)\" as a workaround. "+sassWarningEnd) expectParseError(t, ".card { .nav &__header { color: red } }", ".card {\n .nav &__header {\n color: red;\n }\n}\n", ": WARNING: Cannot use type selector \"__header\" directly after nesting selector \"&\"\n"+ sassWarningStart+"You can move the \"&\" to the end of this selector as a workaround. "+sassWarningEnd) @@ -990,21 +1021,21 @@ func TestNestedSelector(t *testing.T) { expectPrintedLower(t, ".foo { @media screen { &:hover { color: red } } }", "@media screen {\n .foo:hover {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo { @media screen { :hover { color: red } } }", "@media screen {\n .foo :hover {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo, .bar { @media screen { color: red } }", "@media screen {\n .foo,\n .bar {\n color: red;\n }\n}\n") - expectPrintedLower(t, ".foo, .bar { @media screen { &:hover { color: red } } }", "@media screen {\n :is(.foo, .bar):hover {\n color: red;\n }\n}\n") - expectPrintedLower(t, ".foo, .bar { @media screen { :hover { color: red } } }", "@media screen {\n :is(.foo, .bar) :hover {\n color: red;\n }\n}\n") + expectPrintedLower(t, ".foo, .bar { @media screen { &:hover { color: red } } }", "@media screen {\n .foo:hover,\n .bar:hover {\n color: red;\n }\n}\n") + expectPrintedLower(t, ".foo, .bar { @media screen { :hover { color: red } } }", "@media screen {\n .foo :hover,\n .bar :hover {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo { @layer xyz {} }", "@layer xyz;\n") expectPrintedLower(t, ".foo { @layer xyz { color: red } }", "@layer xyz {\n .foo {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo { @layer xyz { &:hover { color: red } } }", "@layer xyz {\n .foo:hover {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo { @layer xyz { :hover { color: red } } }", "@layer xyz {\n .foo :hover {\n color: red;\n }\n}\n") expectPrintedLower(t, ".foo, .bar { @layer xyz { color: red } }", "@layer xyz {\n .foo,\n .bar {\n color: red;\n }\n}\n") - expectPrintedLower(t, ".foo, .bar { @layer xyz { &:hover { color: red } } }", "@layer xyz {\n :is(.foo, .bar):hover {\n color: red;\n }\n}\n") - expectPrintedLower(t, ".foo, .bar { @layer xyz { :hover { color: red } } }", "@layer xyz {\n :is(.foo, .bar) :hover {\n color: red;\n }\n}\n") + expectPrintedLower(t, ".foo, .bar { @layer xyz { &:hover { color: red } } }", "@layer xyz {\n .foo:hover,\n .bar:hover {\n color: red;\n }\n}\n") + expectPrintedLower(t, ".foo, .bar { @layer xyz { :hover { color: red } } }", "@layer xyz {\n .foo :hover,\n .bar :hover {\n color: red;\n }\n}\n") expectPrintedLower(t, "@media screen { @media (min-width: 900px) { a, b { &:hover { color: red } } } }", - "@media screen {\n @media (min-width: 900px) {\n :is(a, b):hover {\n color: red;\n }\n }\n}\n") + "@media screen {\n @media (min-width: 900px) {\n a:hover,\n b:hover {\n color: red;\n }\n }\n}\n") expectPrintedLower(t, "@supports (display: flex) { @supports selector(h2 > p) { a, b { &:hover { color: red } } } }", - "@supports (display: flex) {\n @supports selector(h2 > p) {\n :is(a, b):hover {\n color: red;\n }\n }\n}\n") + "@supports (display: flex) {\n @supports selector(h2 > p) {\n a:hover,\n b:hover {\n color: red;\n }\n }\n}\n") expectPrintedLower(t, "@layer foo { @layer bar { a, b { &:hover { color: red } } } }", - "@layer foo {\n @layer bar {\n :is(a, b):hover {\n color: red;\n }\n }\n}\n") + "@layer foo {\n @layer bar {\n a:hover,\n b:hover {\n color: red;\n }\n }\n}\n") expectPrintedLower(t, ".card { @supports (selector(&)) { &:hover { color: red } } }", "@supports (selector(&)) {\n .card:hover {\n color: red;\n }\n}\n") expectPrintedLower(t, "html { @layer base { color: blue; @layer support { & body { color: red } } } }",