Skip to content

Commit

Permalink
css: emit mappings for nesting selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 16, 2023
1 parent 987b08a commit c6e14ef
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 27 deletions.
14 changes: 9 additions & 5 deletions internal/css_ast/css_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ func (s ComplexSelector) CloneWithoutLeadingCombinator() ComplexSelector {
func (sel ComplexSelector) IsRelative() bool {
if sel.Selectors[0].Combinator.Byte == 0 {
for _, inner := range sel.Selectors {
if inner.HasNestingSelector {
if inner.HasNestingSelector() {
return false
}
for _, ss := range inner.SubclassSelectors {
Expand Down Expand Up @@ -699,7 +699,7 @@ func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck)

for i, ai := range a.Selectors {
bi := b.Selectors[i]
if ai.HasNestingSelector != bi.HasNestingSelector || ai.Combinator.Byte != bi.Combinator.Byte {
if ai.HasNestingSelector() != bi.HasNestingSelector() || ai.Combinator.Byte != bi.Combinator.Byte {
return false
}

Expand Down Expand Up @@ -730,12 +730,16 @@ type Combinator struct {
type CompoundSelector struct {
TypeSelector *NamespacedName
SubclassSelectors []SubclassSelector
Combinator Combinator // Optional, may be 0
HasNestingSelector bool // "&"
NestingSelectorLoc ast.Index32 // "&"
Combinator Combinator // Optional, may be 0
}

func (sel *CompoundSelector) HasNestingSelector() bool {
return sel.NestingSelectorLoc.IsValid()
}

func (sel CompoundSelector) IsSingleAmpersand() bool {
return sel.HasNestingSelector && sel.Combinator.Byte == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
return sel.HasNestingSelector() && sel.Combinator.Byte == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
}

func (sel CompoundSelector) Clone() CompoundSelector {
Expand Down
17 changes: 10 additions & 7 deletions internal/css_parser/css_nesting.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package css_parser

import (
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/css_ast"
)

Expand Down Expand Up @@ -115,7 +116,7 @@ func lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingConte

// Inject the implicit "&" now for simplicity later on
if sel.IsRelative() {
sel.Selectors = append([]css_ast.CompoundSelector{{HasNestingSelector: true}}, sel.Selectors...)
sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLoc: ast.MakeIndex32(uint32(rule.Loc.Start))}}, sel.Selectors...)
}

// Pseudo-elements aren't supported by ":is" (i.e. ":is(div, div::before)"
Expand Down Expand Up @@ -143,7 +144,7 @@ func lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingConte
}

// Are all children of the form "&«something»"?
if first := sel.Selectors[0]; !first.HasNestingSelector || first.IsSingleAmpersand() {
if first := sel.Selectors[0]; !first.HasNestingSelector() || first.IsSingleAmpersand() {
canUseGroupSubSelector = false
}
}
Expand All @@ -152,22 +153,24 @@ func lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingConte
if canUseGroupDescendantCombinator {
// "& a, & b {}" => "& :is(a, b) {}"
// "& > a, & > b {}" => "& > :is(a, b) {}"
nestingSelectorLoc := r.Selectors[0].Selectors[0].NestingSelectorLoc
for i := range r.Selectors {
sel := &r.Selectors[i]
sel.Selectors = sel.Selectors[1:]
}
merged := multipleComplexSelectorsToSingleComplexSelector(r.Selectors)
merged.Selectors = append([]css_ast.CompoundSelector{{HasNestingSelector: true}}, merged.Selectors...)
merged.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLoc: nestingSelectorLoc}}, merged.Selectors...)
r.Selectors = []css_ast.ComplexSelector{merged}
} else if canUseGroupSubSelector {
// "&a, &b {}" => "&:is(a, b) {}"
// "> &a, > &b {}" => "> &:is(a, b) {}"
nestingSelectorLoc := r.Selectors[0].Selectors[0].NestingSelectorLoc
for i := range r.Selectors {
sel := &r.Selectors[i]
sel.Selectors[0].HasNestingSelector = false
sel.Selectors[0].NestingSelectorLoc = ast.Index32{}
}
merged := multipleComplexSelectorsToSingleComplexSelector(r.Selectors)
merged.Selectors[0].HasNestingSelector = true
merged.Selectors[0].NestingSelectorLoc = nestingSelectorLoc
r.Selectors = []css_ast.ComplexSelector{merged}
}

Expand Down Expand Up @@ -237,8 +240,8 @@ const (
)

func substituteAmpersandsInCompoundSelector(sel css_ast.CompoundSelector, replacement css_ast.ComplexSelector, results []css_ast.CompoundSelector, strip leadingCombinatorStrip) []css_ast.CompoundSelector {
if sel.HasNestingSelector {
sel.HasNestingSelector = false
if sel.HasNestingSelector() {
sel.NestingSelectorLoc = ast.Index32{}

// Convert the replacement to a single compound selector
var single css_ast.CompoundSelector
Expand Down
2 changes: 1 addition & 1 deletion internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ var nonDeprecatedElementsSupportedByIE7 = map[string]bool{
func isSafeSelectors(complexSelectors []css_ast.ComplexSelector) bool {
for _, complex := range complexSelectors {
for _, compound := range complex.Selectors {
if compound.HasNestingSelector {
if compound.HasNestingSelector() {
// Bail because this is an extension: https://drafts.csswg.org/css-nesting-1/
return false
}
Expand Down
28 changes: 15 additions & 13 deletions internal/css_parser/css_parser_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ skip:

case canRemoveLeadingAmpersandIfNotFirst:
for i := 1; i < len(list); i++ {
if sel := list[i].Selectors[0]; !sel.HasNestingSelector && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
if sel := list[i].Selectors[0]; !sel.HasNestingSelector() && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
list[0].Selectors = list[0].Selectors[1:]
list[0], list[i] = list[i], list[0]
break
Expand All @@ -85,7 +85,7 @@ func flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.
// done separately from the loop below because inlining may produce
// multiple complex selectors.
if len(sel.Selectors) == 1 {
if single := sel.Selectors[0]; !single.HasNestingSelector && single.TypeSelector == nil && len(single.SubclassSelectors) == 1 && single.Combinator.Byte == 0 {
if single := sel.Selectors[0]; !single.HasNestingSelector() && single.TypeSelector == nil && len(single.SubclassSelectors) == 1 && single.Combinator.Byte == 0 {
if pseudo, ok := single.SubclassSelectors[0].Data.(*css_ast.SSPseudoClassWithSelectorList); ok && (pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal) {
// ":local(.a, .b)" => ".a, .b"
return append(list, pseudo.Selectors...)
Expand All @@ -107,7 +107,7 @@ func flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.
// contents can be inlined, then inline it directly. This has to be
// done separately from the loop below because inlining may produce
// multiple compound selectors.
if !s.HasNestingSelector && s.TypeSelector == nil && len(s.SubclassSelectors) == 1 {
if !s.HasNestingSelector() && s.TypeSelector == nil && len(s.SubclassSelectors) == 1 {
if pseudo, ok := s.SubclassSelectors[0].Data.(*css_ast.SSPseudoClassWithSelectorList); ok &&
(pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal) && len(pseudo.Selectors) == 1 {
if nested := pseudo.Selectors[0].Selectors; ok && (s.Combinator.Byte == 0 || nested[0].Combinator.Byte == 0) {
Expand All @@ -132,9 +132,9 @@ func flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.
// ".foo:local(div)" => "div.foo"
s.TypeSelector = single.TypeSelector
}
if single.HasNestingSelector {
if single.HasNestingSelector() {
// ".foo:local(&)" => "&.foo"
s.HasNestingSelector = true
s.NestingSelectorLoc = single.NestingSelectorLoc
}
// ".foo:local(.bar)" => ".foo.bar"
subclassSelectors = append(subclassSelectors, single.SubclassSelectors...)
Expand Down Expand Up @@ -171,7 +171,7 @@ const (
func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
if len(sel.Selectors) > 1 {
if first := sel.Selectors[0]; first.IsSingleAmpersand() {
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && second.HasNestingSelector {
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && second.HasNestingSelector() {
// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
// "& + div {}" => "+ div {}"
Expand Down Expand Up @@ -264,7 +264,7 @@ func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_a
hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand)
if hasLeadingNestingSelector {
p.reportUseOfNesting(p.current().Range, opts.isDeclarationContext)
sel.HasNestingSelector = true
sel.NestingSelectorLoc = ast.MakeIndex32(uint32(startLoc.Start))
p.advance()
}

Expand Down Expand Up @@ -296,12 +296,14 @@ func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_a
// Parse the subclass selectors
subclassSelectors:
for {
switch p.current().Kind {
subclassToken := p.current()

switch subclassToken.Kind {
case css_lexer.THash:
if (p.current().Flags & css_lexer.IsID) == 0 {
if (subclassToken.Flags & css_lexer.IsID) == 0 {
break subclassSelectors
}
nameLoc := logger.Loc{Start: p.current().Range.Loc.Start + 1}
nameLoc := logger.Loc{Start: subclassToken.Range.Loc.Start + 1}
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Data: &css_ast.SSHash{
Expand Down Expand Up @@ -368,8 +370,8 @@ subclassSelectors:

case css_lexer.TDelimAmpersand:
// This is an extension: https://drafts.csswg.org/css-nesting-1/
p.reportUseOfNesting(p.current().Range, sel.HasNestingSelector)
sel.HasNestingSelector = true
p.reportUseOfNesting(subclassToken.Range, sel.HasNestingSelector())
sel.NestingSelectorLoc = ast.MakeIndex32(uint32(subclassToken.Range.Loc.Start))
p.advance()

default:
Expand All @@ -378,7 +380,7 @@ subclassSelectors:
}

// The compound selector must be non-empty
if !sel.HasNestingSelector && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 {
if !sel.HasNestingSelector() && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 {
p.unexpected()
return
}
Expand Down
6 changes: 5 additions & 1 deletion internal/css_printer/css_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,11 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo
p.printNamespacedName(*sel.TypeSelector, whitespace)
}

if sel.HasNestingSelector {
if sel.HasNestingSelector() {
if p.options.AddSourceMappings {
p.builder.AddSourceMapping(logger.Loc{Start: int32(sel.NestingSelectorLoc.GetIndex())}, "", p.css)
}

p.print("&")
}

Expand Down

0 comments on commit c6e14ef

Please sign in to comment.