Skip to content

Commit 69996b4

Browse files
committed
add initial support for "@nest" rules (#1945)
1 parent 9851b5a commit 69996b4

File tree

5 files changed

+81
-15
lines changed

5 files changed

+81
-15
lines changed

internal/css_ast/css_ast.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ func (r *RUnknownAt) Hash() (uint32, bool) {
407407
type RSelector struct {
408408
Selectors []ComplexSelector
409409
Rules []Rule
410+
HasAtNest bool
410411
}
411412

412413
func (a *RSelector) Equal(rule R) bool {

internal/css_parser/css_parser.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ loop:
250250
}
251251

252252
if context.parseSelectors {
253-
rules = append(rules, p.parseSelectorRule())
253+
rules = append(rules, p.parseSelectorRuleFrom(p.index, parseSelectorOpts{}))
254254
} else {
255255
rules = append(rules, p.parseQualifiedRuleFrom(p.index, false /* isAlreadyInvalid */))
256256
}
@@ -282,7 +282,7 @@ func (p *parser) parseListOfDeclarations() (list []css_ast.Rule) {
282282

283283
case css_lexer.TDelimAmpersand:
284284
// Reference: https://drafts.csswg.org/css-nesting-1/
285-
list = append(list, p.parseSelectorRule())
285+
list = append(list, p.parseSelectorRuleFrom(p.index, parseSelectorOpts{}))
286286

287287
default:
288288
list = append(list, p.parseDeclaration())
@@ -614,6 +614,9 @@ var specialAtRules = map[string]atRuleKind{
614614
"media": atRuleInheritContext,
615615
"scope": atRuleInheritContext,
616616
"supports": atRuleInheritContext,
617+
618+
// Reference: https://drafts.csswg.org/css-nesting-1/
619+
"nest": atRuleDeclarations,
617620
}
618621

619622
type atRuleValidity uint8
@@ -815,6 +818,14 @@ func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
815818
}}
816819
}
817820

821+
case "nest":
822+
// Reference: https://drafts.csswg.org/css-nesting-1/
823+
p.eat(css_lexer.TWhitespace)
824+
if kind := p.current().Kind; kind != css_lexer.TSemicolon && kind != css_lexer.TOpenBrace &&
825+
kind != css_lexer.TCloseBrace && kind != css_lexer.TEndOfFile {
826+
return p.parseSelectorRuleFrom(preludeStart-1, parseSelectorOpts{atNestRange: atRange})
827+
}
828+
818829
default:
819830
if kind == atRuleUnknown && atToken == "namespace" {
820831
// CSS namespaces are a weird feature that appears to only really be
@@ -1217,15 +1228,31 @@ func mangleNumber(t string) (string, bool) {
12171228
return t, t != original
12181229
}
12191230

1220-
func (p *parser) parseSelectorRule() css_ast.Rule {
1221-
preludeStart := p.index
1222-
1231+
func (p *parser) parseSelectorRuleFrom(preludeStart int, opts parseSelectorOpts) css_ast.Rule {
12231232
// Try parsing the prelude as a selector list
1224-
if list, ok := p.parseSelectorList(); ok {
1225-
selector := css_ast.RSelector{Selectors: list}
1233+
if list, ok := p.parseSelectorList(opts); ok {
1234+
selector := css_ast.RSelector{
1235+
Selectors: list,
1236+
HasAtNest: opts.atNestRange.Len != 0,
1237+
}
12261238
if p.expect(css_lexer.TOpenBrace) {
12271239
selector.Rules = p.parseListOfDeclarations()
12281240
p.expect(css_lexer.TCloseBrace)
1241+
1242+
// Minify "@nest" when possible
1243+
if p.options.MangleSyntax && selector.HasAtNest {
1244+
allHaveNestPrefix := true
1245+
for _, complex := range selector.Selectors {
1246+
if len(complex.Selectors) == 0 || !complex.Selectors[0].HasNestPrefix {
1247+
allHaveNestPrefix = false
1248+
break
1249+
}
1250+
}
1251+
if allHaveNestPrefix {
1252+
selector.HasAtNest = false
1253+
}
1254+
}
1255+
12291256
return css_ast.Rule{Loc: p.tokens[preludeStart].Range.Loc, Data: &selector}
12301257
}
12311258
}

internal/css_parser/css_parser_selector.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"github.com/evanw/esbuild/internal/logger"
1010
)
1111

12-
func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) {
12+
func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) {
1313
// Parse the first selector
1414
firstRange := p.current().Range
15-
sel, good, firstHasNestPrefix := p.parseComplexSelector()
15+
sel, good, firstHasNestPrefix := p.parseComplexSelector(opts)
1616
if !good {
1717
return
1818
}
@@ -26,14 +26,14 @@ func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) {
2626
}
2727
p.eat(css_lexer.TWhitespace)
2828
loc := p.current().Range.Loc
29-
sel, good, hasNestPrefix := p.parseComplexSelector()
29+
sel, good, hasNestPrefix := p.parseComplexSelector(opts)
3030
if !good {
3131
return
3232
}
3333
list = append(list, sel)
3434

3535
// Validate nest prefix consistency
36-
if firstHasNestPrefix && !hasNestPrefix {
36+
if firstHasNestPrefix && !hasNestPrefix && opts.atNestRange.Len == 0 {
3737
data := p.tracker.MsgData(logger.Range{Loc: loc}, "Every selector in a nested style rule must start with \"&\"")
3838
data.Location.Suggestion = "&"
3939
p.log.AddMsg(logger.Msg{
@@ -48,13 +48,19 @@ func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) {
4848
return
4949
}
5050

51-
func (p *parser) parseComplexSelector() (result css_ast.ComplexSelector, ok bool, hasNestPrefix bool) {
51+
type parseSelectorOpts struct {
52+
atNestRange logger.Range
53+
}
54+
55+
func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.ComplexSelector, ok bool, hasNestPrefix bool) {
5256
// Parent
57+
loc := p.current().Range.Loc
5358
sel, good := p.parseCompoundSelector()
5459
if !good {
5560
return
5661
}
5762
hasNestPrefix = sel.HasNestPrefix
63+
hasNestSelector := sel.HasNestPrefix
5864
result.Selectors = append(result.Selectors, sel)
5965

6066
for {
@@ -76,6 +82,15 @@ func (p *parser) parseComplexSelector() (result css_ast.ComplexSelector, ok bool
7682
}
7783
sel.Combinator = combinator
7884
result.Selectors = append(result.Selectors, sel)
85+
if sel.HasNestPrefix {
86+
hasNestSelector = true
87+
}
88+
}
89+
90+
// Validate nest selector consistency
91+
if opts.atNestRange.Len != 0 && !hasNestSelector {
92+
p.log.AddWithNotes(logger.Warning, &p.tracker, logger.Range{Loc: loc}, "Every selector in a nested style rule must contain \"&\"",
93+
[]logger.MsgData{p.tracker.MsgData(opts.atNestRange, "This is a nested style rule because of the \"@nest\" here:")})
7994
}
8095

8196
ok = true

internal/css_parser/css_parser_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,26 @@ func TestNestedSelector(t *testing.T) {
693693
"<stdin>: WARNING: Every selector in a nested style rule must start with \"&\"\n"+
694694
"<stdin>: NOTE: This is a nested style rule because of the \"&\" here:\n")
695695
expectParseError(t, "a { & b, & c {} }", "")
696+
697+
expectParseError(t, "a { b & {} }", "<stdin>: WARNING: Expected \":\"\n")
698+
expectParseError(t, "a { @nest b & {} }", "")
699+
expectParseError(t, "a { @nest & b, c {} }",
700+
"<stdin>: WARNING: Every selector in a nested style rule must contain \"&\"\n"+
701+
"<stdin>: NOTE: This is a nested style rule because of the \"@nest\" here:\n")
702+
expectParseError(t, "a { @nest b &, c {} }",
703+
"<stdin>: WARNING: Every selector in a nested style rule must contain \"&\"\n"+
704+
"<stdin>: NOTE: This is a nested style rule because of the \"@nest\" here:\n")
705+
expectPrinted(t, "a { @nest b & { color: red } }", "a {\n @nest b & {\n color: red;\n }\n}\n")
706+
707+
// Don't drop "@nest" for invalid rules
708+
expectParseError(t, "a { @nest @invalid { color: red } }", "<stdin>: WARNING: Unexpected \"@invalid\"\n")
709+
expectPrinted(t, "a { @nest @invalid { color: red } }", "a {\n @nest @invalid {\n color: red;\n }\n}\n")
710+
711+
// Check removal of "@nest" when minifying
712+
expectPrinted(t, "a { @nest & b, & c { color: red } }", "a {\n @nest & b,\n & c {\n color: red;\n }\n}\n")
713+
expectPrintedMangle(t, "a { @nest & b, & c { color: red } }", "a {\n & b,\n & c {\n color: red;\n }\n}\n")
714+
expectPrintedMangle(t, "a { @nest b &, & c { color: red } }", "a {\n @nest b &,\n & c {\n color: red;\n }\n}\n")
715+
expectPrintedMangle(t, "a { @nest & b, c & { color: red } }", "a {\n @nest & b,\n c & {\n color: red;\n }\n}\n")
696716
}
697717

698718
func TestBadQualifiedRules(t *testing.T) {

internal/css_printer/css_printer.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicol
187187
}
188188

189189
case *css_ast.RSelector:
190-
p.printComplexSelectors(r.Selectors, indent)
190+
if r.HasAtNest {
191+
p.print("@nest")
192+
}
193+
p.printComplexSelectors(r.Selectors, indent, r.HasAtNest)
191194
if !p.options.RemoveWhitespace {
192195
p.print(" ")
193196
}
@@ -272,7 +275,7 @@ func (p *printer) printRuleBlock(rules []css_ast.Rule, indent int32) {
272275
p.print("}")
273276
}
274277

275-
func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32) {
278+
func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32, hasAtNest bool) {
276279
for i, complex := range selectors {
277280
if i > 0 {
278281
if p.options.RemoveWhitespace {
@@ -284,7 +287,7 @@ func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, ind
284287
}
285288

286289
for j, compound := range complex.Selectors {
287-
p.printCompoundSelector(compound, j == 0, j+1 == len(complex.Selectors))
290+
p.printCompoundSelector(compound, (!hasAtNest || i != 0) && j == 0, j+1 == len(complex.Selectors))
288291
}
289292
}
290293
}

0 commit comments

Comments
 (0)