Skip to content

Commit

Permalink
add initial support for "@nest" rules (#1945)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 20, 2022
1 parent 9851b5a commit 69996b4
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 15 deletions.
1 change: 1 addition & 0 deletions internal/css_ast/css_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
41 changes: 34 additions & 7 deletions internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 */))
}
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
}
}
Expand Down
25 changes: 20 additions & 5 deletions internal/css_parser/css_parser_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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{
Expand All @@ -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 {
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions internal/css_parser/css_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,26 @@ func TestNestedSelector(t *testing.T) {
"<stdin>: WARNING: Every selector in a nested style rule must start with \"&\"\n"+
"<stdin>: NOTE: This is a nested style rule because of the \"&\" here:\n")
expectParseError(t, "a { & b, & c {} }", "")

expectParseError(t, "a { b & {} }", "<stdin>: WARNING: Expected \":\"\n")
expectParseError(t, "a { @nest b & {} }", "")
expectParseError(t, "a { @nest & b, c {} }",
"<stdin>: WARNING: Every selector in a nested style rule must contain \"&\"\n"+
"<stdin>: NOTE: This is a nested style rule because of the \"@nest\" here:\n")
expectParseError(t, "a { @nest b &, c {} }",
"<stdin>: WARNING: Every selector in a nested style rule must contain \"&\"\n"+
"<stdin>: 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 } }", "<stdin>: 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) {
Expand Down
9 changes: 6 additions & 3 deletions internal/css_printer/css_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ")
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
}
}
Expand Down

0 comments on commit 69996b4

Please sign in to comment.