From 69996b448db54d1106aeeb3aa6e6d891d49c4340 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 20 Jan 2022 16:51:03 +0000 Subject: [PATCH] add initial support for "@nest" rules (#1945) --- internal/css_ast/css_ast.go | 1 + internal/css_parser/css_parser.go | 41 ++++++++++++++++++---- internal/css_parser/css_parser_selector.go | 25 ++++++++++--- internal/css_parser/css_parser_test.go | 20 +++++++++++ internal/css_printer/css_printer.go | 9 +++-- 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/internal/css_ast/css_ast.go b/internal/css_ast/css_ast.go index 7e6022d701c..56b62eb33ec 100644 --- a/internal/css_ast/css_ast.go +++ b/internal/css_ast/css_ast.go @@ -407,6 +407,7 @@ func (r *RUnknownAt) Hash() (uint32, bool) { type RSelector struct { Selectors []ComplexSelector Rules []Rule + HasAtNest bool } func (a *RSelector) Equal(rule R) bool { diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 75d7ef8c30c..53e09277ced 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -250,7 +250,7 @@ loop: } if context.parseSelectors { - rules = append(rules, p.parseSelectorRule()) + rules = append(rules, p.parseSelectorRuleFrom(p.index, parseSelectorOpts{})) } else { rules = append(rules, p.parseQualifiedRuleFrom(p.index, false /* isAlreadyInvalid */)) } @@ -282,7 +282,7 @@ func (p *parser) parseListOfDeclarations() (list []css_ast.Rule) { case css_lexer.TDelimAmpersand: // Reference: https://drafts.csswg.org/css-nesting-1/ - list = append(list, p.parseSelectorRule()) + list = append(list, p.parseSelectorRuleFrom(p.index, parseSelectorOpts{})) default: list = append(list, p.parseDeclaration()) @@ -614,6 +614,9 @@ var specialAtRules = map[string]atRuleKind{ "media": atRuleInheritContext, "scope": atRuleInheritContext, "supports": atRuleInheritContext, + + // Reference: https://drafts.csswg.org/css-nesting-1/ + "nest": atRuleDeclarations, } type atRuleValidity uint8 @@ -815,6 +818,14 @@ func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule { }} } + case "nest": + // Reference: https://drafts.csswg.org/css-nesting-1/ + p.eat(css_lexer.TWhitespace) + if kind := p.current().Kind; kind != css_lexer.TSemicolon && kind != css_lexer.TOpenBrace && + kind != css_lexer.TCloseBrace && kind != css_lexer.TEndOfFile { + return p.parseSelectorRuleFrom(preludeStart-1, parseSelectorOpts{atNestRange: atRange}) + } + default: if kind == atRuleUnknown && atToken == "namespace" { // CSS namespaces are a weird feature that appears to only really be @@ -1217,15 +1228,31 @@ func mangleNumber(t string) (string, bool) { return t, t != original } -func (p *parser) parseSelectorRule() css_ast.Rule { - preludeStart := p.index - +func (p *parser) parseSelectorRuleFrom(preludeStart int, opts parseSelectorOpts) css_ast.Rule { // Try parsing the prelude as a selector list - if list, ok := p.parseSelectorList(); ok { - selector := css_ast.RSelector{Selectors: list} + if list, ok := p.parseSelectorList(opts); ok { + selector := css_ast.RSelector{ + Selectors: list, + HasAtNest: opts.atNestRange.Len != 0, + } if p.expect(css_lexer.TOpenBrace) { selector.Rules = p.parseListOfDeclarations() p.expect(css_lexer.TCloseBrace) + + // Minify "@nest" when possible + if p.options.MangleSyntax && selector.HasAtNest { + allHaveNestPrefix := true + for _, complex := range selector.Selectors { + if len(complex.Selectors) == 0 || !complex.Selectors[0].HasNestPrefix { + allHaveNestPrefix = false + break + } + } + if allHaveNestPrefix { + selector.HasAtNest = false + } + } + return css_ast.Rule{Loc: p.tokens[preludeStart].Range.Loc, Data: &selector} } } diff --git a/internal/css_parser/css_parser_selector.go b/internal/css_parser/css_parser_selector.go index b63f7ccaef2..85fce8719bd 100644 --- a/internal/css_parser/css_parser_selector.go +++ b/internal/css_parser/css_parser_selector.go @@ -9,10 +9,10 @@ import ( "github.com/evanw/esbuild/internal/logger" ) -func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) { +func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) { // Parse the first selector firstRange := p.current().Range - sel, good, firstHasNestPrefix := p.parseComplexSelector() + sel, good, firstHasNestPrefix := p.parseComplexSelector(opts) if !good { return } @@ -26,14 +26,14 @@ func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) { } p.eat(css_lexer.TWhitespace) loc := p.current().Range.Loc - sel, good, hasNestPrefix := p.parseComplexSelector() + sel, good, hasNestPrefix := p.parseComplexSelector(opts) if !good { return } list = append(list, sel) // Validate nest prefix consistency - if firstHasNestPrefix && !hasNestPrefix { + if firstHasNestPrefix && !hasNestPrefix && opts.atNestRange.Len == 0 { data := p.tracker.MsgData(logger.Range{Loc: loc}, "Every selector in a nested style rule must start with \"&\"") data.Location.Suggestion = "&" p.log.AddMsg(logger.Msg{ @@ -48,13 +48,19 @@ func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) { return } -func (p *parser) parseComplexSelector() (result css_ast.ComplexSelector, ok bool, hasNestPrefix bool) { +type parseSelectorOpts struct { + atNestRange logger.Range +} + +func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.ComplexSelector, ok bool, hasNestPrefix bool) { // Parent + loc := p.current().Range.Loc sel, good := p.parseCompoundSelector() if !good { return } hasNestPrefix = sel.HasNestPrefix + hasNestSelector := sel.HasNestPrefix result.Selectors = append(result.Selectors, sel) for { @@ -76,6 +82,15 @@ func (p *parser) parseComplexSelector() (result css_ast.ComplexSelector, ok bool } sel.Combinator = combinator result.Selectors = append(result.Selectors, sel) + if sel.HasNestPrefix { + hasNestSelector = true + } + } + + // Validate nest selector consistency + if opts.atNestRange.Len != 0 && !hasNestSelector { + 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:")}) } ok = true diff --git a/internal/css_parser/css_parser_test.go b/internal/css_parser/css_parser_test.go index e74c43cfdab..84fe1979fef 100644 --- a/internal/css_parser/css_parser_test.go +++ b/internal/css_parser/css_parser_test.go @@ -693,6 +693,26 @@ func TestNestedSelector(t *testing.T) { ": WARNING: Every selector in a nested style rule must start with \"&\"\n"+ ": NOTE: This is a nested style rule because of the \"&\" here:\n") expectParseError(t, "a { & b, & c {} }", "") + + expectParseError(t, "a { b & {} }", ": WARNING: Expected \":\"\n") + expectParseError(t, "a { @nest b & {} }", "") + expectParseError(t, "a { @nest & b, c {} }", + ": WARNING: Every selector in a nested style rule must contain \"&\"\n"+ + ": NOTE: This is a nested style rule because of the \"@nest\" here:\n") + expectParseError(t, "a { @nest b &, c {} }", + ": 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") + + // Don't drop "@nest" for invalid rules + expectParseError(t, "a { @nest @invalid { color: red } }", ": WARNING: Unexpected \"@invalid\"\n") + expectPrinted(t, "a { @nest @invalid { color: red } }", "a {\n @nest @invalid {\n color: red;\n }\n}\n") + + // Check removal of "@nest" when minifying + expectPrinted(t, "a { @nest & b, & c { color: red } }", "a {\n @nest & b,\n & c {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "a { @nest & b, & c { color: red } }", "a {\n & b,\n & c {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "a { @nest b &, & c { color: red } }", "a {\n @nest b &,\n & c {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "a { @nest & b, c & { color: red } }", "a {\n @nest & b,\n c & {\n color: red;\n }\n}\n") } func TestBadQualifiedRules(t *testing.T) { diff --git a/internal/css_printer/css_printer.go b/internal/css_printer/css_printer.go index e2698a56e3c..46519fee720 100644 --- a/internal/css_printer/css_printer.go +++ b/internal/css_printer/css_printer.go @@ -187,7 +187,10 @@ func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicol } case *css_ast.RSelector: - p.printComplexSelectors(r.Selectors, indent) + if r.HasAtNest { + p.print("@nest") + } + p.printComplexSelectors(r.Selectors, indent, r.HasAtNest) if !p.options.RemoveWhitespace { p.print(" ") } @@ -272,7 +275,7 @@ func (p *printer) printRuleBlock(rules []css_ast.Rule, indent int32) { p.print("}") } -func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32) { +func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32, hasAtNest bool) { for i, complex := range selectors { if i > 0 { if p.options.RemoveWhitespace { @@ -284,7 +287,7 @@ func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, ind } for j, compound := range complex.Selectors { - p.printCompoundSelector(compound, j == 0, j+1 == len(complex.Selectors)) + p.printCompoundSelector(compound, (!hasAtNest || i != 0) && j == 0, j+1 == len(complex.Selectors)) } } }