diff --git a/internal/css_ast/css_ast.go b/internal/css_ast/css_ast.go index 56b62eb33ec..bc0fd4d3c98 100644 --- a/internal/css_ast/css_ast.go +++ b/internal/css_ast/css_ast.go @@ -412,7 +412,7 @@ type RSelector struct { func (a *RSelector) Equal(rule R) bool { b, ok := rule.(*RSelector) - if ok && len(a.Selectors) == len(b.Selectors) { + if ok && len(a.Selectors) == len(b.Selectors) && a.HasAtNest == b.HasAtNest { for i, sel := range a.Selectors { if !sel.Equal(b.Selectors[i]) { return false @@ -524,7 +524,7 @@ func (a ComplexSelector) Equal(b ComplexSelector) bool { for i, ai := range a.Selectors { bi := b.Selectors[i] - if ai.HasNestPrefix != bi.HasNestPrefix || ai.Combinator != bi.Combinator { + if ai.NestingSelector != bi.NestingSelector || ai.Combinator != bi.Combinator { return false } @@ -547,11 +547,19 @@ func (a ComplexSelector) Equal(b ComplexSelector) bool { return true } +type NestingSelector uint8 + +const ( + NestingSelectorNone NestingSelector = iota + NestingSelectorPrefix // "&a {}" + NestingSelectorPresentButNotPrefix // "a& {}" +) + type CompoundSelector struct { Combinator string // Optional, may be "" TypeSelector *NamespacedName SubclassSelectors []SS - HasNestPrefix bool // "&" + NestingSelector NestingSelector // "&" } type NameToken struct { diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 53e09277ced..19e47065d50 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -473,7 +473,7 @@ var nonDeprecatedElementsSupportedByIE7 = map[string]bool{ func isSafeSelectors(complexSelectors []css_ast.ComplexSelector) bool { for _, complex := range complexSelectors { for _, compound := range complex.Selectors { - if compound.HasNestPrefix { + if compound.NestingSelector != css_ast.NestingSelectorNone { // Bail because this is an extension: https://drafts.csswg.org/css-nesting-1/ return false } @@ -1243,7 +1243,7 @@ func (p *parser) parseSelectorRuleFrom(preludeStart int, opts parseSelectorOpts) if p.options.MangleSyntax && selector.HasAtNest { allHaveNestPrefix := true for _, complex := range selector.Selectors { - if len(complex.Selectors) == 0 || !complex.Selectors[0].HasNestPrefix { + if len(complex.Selectors) == 0 || complex.Selectors[0].NestingSelector != css_ast.NestingSelectorPrefix { allHaveNestPrefix = false break } diff --git a/internal/css_parser/css_parser_selector.go b/internal/css_parser/css_parser_selector.go index 85fce8719bd..24b82635ec4 100644 --- a/internal/css_parser/css_parser_selector.go +++ b/internal/css_parser/css_parser_selector.go @@ -59,8 +59,8 @@ func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.Co if !good { return } - hasNestPrefix = sel.HasNestPrefix - hasNestSelector := sel.HasNestPrefix + hasNestPrefix = sel.NestingSelector == css_ast.NestingSelectorPrefix + isNestContaining := sel.NestingSelector != css_ast.NestingSelectorNone result.Selectors = append(result.Selectors, sel) for { @@ -82,13 +82,13 @@ func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.Co } sel.Combinator = combinator result.Selectors = append(result.Selectors, sel) - if sel.HasNestPrefix { - hasNestSelector = true + if sel.NestingSelector != css_ast.NestingSelectorNone { + isNestContaining = true } } // Validate nest selector consistency - if opts.atNestRange.Len != 0 && !hasNestSelector { + if opts.atNestRange.Len != 0 && !isNestContaining { p.log.AddWithNotes(logger.Warning, &p.tracker, logger.Range{Loc: loc}, "Every selector in a nested style rule must contain \"&\"", []logger.MsgData{p.tracker.MsgData(opts.atNestRange, "This is a nested style rule because of the \"@nest\" here:")}) } @@ -104,20 +104,21 @@ func (p *parser) nameToken() css_ast.NameToken { } } +func (p *parser) warnNestingUnsupported(r logger.Range) { + 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.Add(logger.Warning, &p.tracker, r, text) +} + func (p *parser) parseCompoundSelector() (sel css_ast.CompoundSelector, ok bool) { // This is an extension: https://drafts.csswg.org/css-nesting-1/ r := p.current().Range if p.eat(css_lexer.TDelimAmpersand) { - sel.HasNestPrefix = true - - // Warn if we're targeting a browser, since it won't work + sel.NestingSelector = css_ast.NestingSelectorPrefix if p.options.UnsupportedCSSFeatures.Has(compat.Nesting) { - where := "the configured target environment" - if p.options.OriginalTargetEnv != "" { - where = fmt.Sprintf("%s (%s)", where, p.options.OriginalTargetEnv) - } - p.log.Add(logger.Warning, &p.tracker, r, fmt.Sprintf( - "CSS nesting syntax is not supported in %s", where)) + p.warnNestingUnsupported(r) } } @@ -201,13 +202,23 @@ subclassSelectors: pseudo := p.parsePseudoClassSelector() sel.SubclassSelectors = append(sel.SubclassSelectors, &pseudo) + case css_lexer.TDelimAmpersand: + // This is an extension: https://drafts.csswg.org/css-nesting-1/ + r := p.current().Range + if p.eat(css_lexer.TDelimAmpersand) && sel.NestingSelector == css_ast.NestingSelectorNone { + sel.NestingSelector = css_ast.NestingSelectorPresentButNotPrefix + if p.options.UnsupportedCSSFeatures.Has(compat.Nesting) { + p.warnNestingUnsupported(r) + } + } + default: break subclassSelectors } } // The compound selector must be non-empty - if !sel.HasNestPrefix && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 { + if sel.NestingSelector == css_ast.NestingSelectorNone && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 { p.unexpected() return } diff --git a/internal/css_parser/css_parser_test.go b/internal/css_parser/css_parser_test.go index 84fe1979fef..2435af7ea57 100644 --- a/internal/css_parser/css_parser_test.go +++ b/internal/css_parser/css_parser_test.go @@ -63,6 +63,13 @@ func expectPrintedLower(t *testing.T, contents string, expected string) { }) } +func expectPrintedMinify(t *testing.T, contents string, expected string) { + t.Helper() + expectPrintedCommon(t, contents+" [minify]", contents, expected, config.Options{ + RemoveWhitespace: true, + }) +} + func expectPrintedMangle(t *testing.T, contents string, expected string) { t.Helper() expectPrintedCommon(t, contents+" [mangle]", contents, expected, config.Options{ @@ -703,6 +710,12 @@ func TestNestedSelector(t *testing.T) { ": WARNING: Every selector in a nested style rule must contain \"&\"\n"+ ": NOTE: This is a nested style rule because of the \"@nest\" here:\n") expectPrinted(t, "a { @nest b & { color: red } }", "a {\n @nest b & {\n color: red;\n }\n}\n") + expectPrinted(t, "a { @nest b& { color: red } }", "a {\n @nest b& {\n color: red;\n }\n}\n") + expectPrinted(t, "a { @nest b&[c] { color: red } }", "a {\n @nest b[c]& {\n color: red;\n }\n}\n") + expectPrinted(t, "a { @nest &[c] { color: red } }", "a {\n @nest &[c] {\n color: red;\n }\n}\n") + expectPrinted(t, "a { @nest [c]& { color: red } }", "a {\n @nest [c]& {\n color: red;\n }\n}\n") + expectPrintedMinify(t, "a { @nest b & { color: red } }", "a{@nest b &{color:red}}") + expectPrintedMinify(t, "a { @nest b& { color: red } }", "a{@nest b&{color:red}}") // Don't drop "@nest" for invalid rules expectParseError(t, "a { @nest @invalid { color: red } }", ": WARNING: Unexpected \"@invalid\"\n") diff --git a/internal/css_printer/css_printer.go b/internal/css_printer/css_printer.go index 46519fee720..37b765ec426 100644 --- a/internal/css_printer/css_printer.go +++ b/internal/css_printer/css_printer.go @@ -300,7 +300,7 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo p.print(" ") } - if sel.HasNestPrefix { + if sel.NestingSelector == css_ast.NestingSelectorPrefix { p.print("&") } @@ -378,6 +378,12 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo p.printPseudoClassSelector(*s, whitespace) } } + + // It doesn't matter where the "&" goes since all non-prefix cases are + // treated the same. This just always puts it as a suffix for simplicity. + if sel.NestingSelector == css_ast.NestingSelectorPresentButNotPrefix { + p.print("&") + } } func (p *printer) printNamespacedName(nsName css_ast.NamespacedName, whitespace trailingWhitespace) {