package css_parser import ( "fmt" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/css_ast" "github.com/evanw/esbuild/internal/logger" ) func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) []css_ast.Rule { switch r := rule.Data.(type) { case *css_ast.RSelector: scope := func(loc logger.Loc) css_ast.ComplexSelector { return css_ast.ComplexSelector{ Selectors: []css_ast.CompoundSelector{{ SubclassSelectors: []css_ast.SubclassSelector{{ Range: logger.Range{Loc: loc}, Data: &css_ast.SSPseudoClass{Name: "scope"}, }}, }}, } } parentSelectorsWithPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors)) parentSelectorsNoPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors)) for i, sel := range r.Selectors { // Top-level "&" should be replaced with ":scope" to avoid recursion. // From https://www.w3.org/TR/css-nesting-1/#nest-selector: // // "When used in the selector of a nested style rule, the nesting // selector represents the elements matched by the parent rule. When // used in any other context, it represents the same elements as // :scope in that context (unless otherwise defined)." // substituted := make([]css_ast.CompoundSelector, 0, len(sel.Selectors)) for _, x := range sel.Selectors { substituted = p.substituteAmpersandsInCompoundSelector(x, scope, substituted, keepLeadingCombinator) } r.Selectors[i] = css_ast.ComplexSelector{Selectors: substituted} // Filter out pseudo elements because they are ignored by nested style // rules. This is because pseudo-elements are not valid within :is(): // https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction // may be relaxed in the future, but this restriction has shipped so // we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433. // // Note: This is only for the parent selector list that is used to // substitute "&" within child rules. Do not filter out the pseudo // element from the top-level selector list. if !sel.UsesPseudoElement() { parentSelectorsNoPseudo = append(parentSelectorsNoPseudo, css_ast.ComplexSelector{Selectors: substituted}) } // This filtering is only done conditionally because it seems to only // apply sometimes. Specifically it doesn't seem to apply when the // nested rule is an at-rule. So we use the unfiltered list in that // case. See: https://github.com/evanw/esbuild/issues/4265 parentSelectorsWithPseudo = append(parentSelectorsWithPseudo, css_ast.ComplexSelector{Selectors: substituted}) } // Emit this selector before its nested children start := len(results) results = append(results, rule) // Lower all children and filter out ones that become empty context := lowerNestingContext{ parentSelectorsWithPseudo: parentSelectorsWithPseudo, parentSelectorsNoPseudo: parentSelectorsNoPseudo, loweredRules: results, } r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &context) // Omit this selector entirely if it's now empty if len(r.Rules) == 0 { copy(context.loweredRules[start:], context.loweredRules[start+1:]) context.loweredRules = context.loweredRules[:len(context.loweredRules)-1] } return context.loweredRules case *css_ast.RKnownAt: var rules []css_ast.Rule for _, child := range r.Rules { rules = p.lowerNestingInRule(child, rules) } r.Rules = rules case *css_ast.RAtLayer: var rules []css_ast.Rule for _, child := range r.Rules { rules = p.lowerNestingInRule(child, rules) } r.Rules = rules case *css_ast.RAtMedia: var rules []css_ast.Rule for _, child := range r.Rules { rules = p.lowerNestingInRule(child, rules) } r.Rules = rules } return append(results, rule) } // Lower all children and filter out ones that become empty func (p *parser) lowerNestingInRulesAndReturnRemaining(rules []css_ast.Rule, context *lowerNestingContext) []css_ast.Rule { n := 0 for _, child := range rules { child = p.lowerNestingInRuleWithContext(child, context) if child.Data != nil { rules[n] = child n++ } } return rules[:n] } func compoundSelectorTermCount(sel css_ast.CompoundSelector) int { count := 0 for _, ss := range sel.SubclassSelectors { count++ if list, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok { count += complexSelectorTermCount(list.Selectors) } } return count } func complexSelectorTermCount(selectors []css_ast.ComplexSelector) int { count := 0 for _, sel := range selectors { for _, inner := range sel.Selectors { count += compoundSelectorTermCount(inner) } } return count } func (p *parser) addExpansionError(loc logger.Loc, n int) { p.log.AddErrorWithNotes(&p.tracker, logger.Range{Loc: loc}, "CSS nesting is causing too much expansion", []logger.MsgData{{Text: fmt.Sprintf("CSS nesting expansion was terminated because a rule was generated with %d selectors. "+ "This limit exists to prevent esbuild from using too much time and/or memory. "+ "Please change your CSS to use fewer levels of nesting.", n)}}) } type lowerNestingContext struct { parentSelectorsWithPseudo []css_ast.ComplexSelector parentSelectorsNoPseudo []css_ast.ComplexSelector loweredRules []css_ast.Rule } func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingContext) css_ast.Rule { switch r := rule.Data.(type) { case *css_ast.RSelector: oldSelectorsLen := len(r.Selectors) oldSelectorsComplexity := complexSelectorTermCount(r.Selectors) // "a { & b {} }" => "a b {}" // "a { &b {} }" => "a:is(b) {}" // "a { &:hover {} }" => "a:hover {}" // ".x { &b {} }" => "b.x {}" // "a, b { .c, d {} }" => ":is(a, b) :is(.c, d) {}" // "a, b { &.c, & d, e & {} }" => ":is(a, b).c, :is(a, b) d, e :is(a, b) {}" // Pass 1: Canonicalize and analyze our selectors for i := range r.Selectors { sel := &r.Selectors[i] // Inject the implicit "&" now for simplicity later on if sel.IsRelative() { sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLocs: []logger.Loc{rule.Loc}}}, sel.Selectors...) } } // Pass 2: Substitute "&" for the parent selector if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectorsNoPseudo) <= 1 { // If we can use ":is", or we don't have to because there's only one // parent selector, or we are using ":is()" to match zero parent selectors // (even if ":is" is unsupported), then substituting "&" for the parent // selector is easy. for i := range r.Selectors { complex := &r.Selectors[i] results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectorsNoPseudo) for _, compound := range complex.Selectors { results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator) } complex.Selectors = results } } else { // Otherwise if we can't use ":is", the transform is more complicated. // Avoiding ":is" can lead to a combinatorial explosion of cases so we // want to avoid this if possible. For example: // // .first, .second, .third { // & > & { // color: red; // } // } // // If we can use ":is" (the easy case above) then we can do this: // // :is(.first, .second, .third) > :is(.first, .second, .third) { // color: red; // } // // But if we can't use ":is" then we have to do this instead: // // .first > .first, // .first > .second, // .first > .third, // .second > .first, // .second > .second, // .second > .third, // .third > .first, // .third > .second, // .third > .third { // color: red; // } // // That combinatorial explosion is what the loop below implements. Note // that PostCSS's implementation of nesting gets this wrong. It generates // this instead: // // .first > .first, // .second > .second, // .third > .third { // color: red; // } // // That's not equivalent, so that's an incorrect transformation. var selectors []css_ast.ComplexSelector var indices []int for { // Every time we encounter another "&", add another dimension offset := 0 parent := func(loc logger.Loc) css_ast.ComplexSelector { if offset == len(indices) { indices = append(indices, 0) } index := indices[offset] offset++ return context.parentSelectorsNoPseudo[index] } // Do the substitution for this particular combination for i := range r.Selectors { complex := r.Selectors[i] results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) for _, compound := range complex.Selectors { results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator) } complex.Selectors = results selectors = append(selectors, complex) offset = 0 } // Do addition with carry on the indices across dimensions carry := len(indices) for carry > 0 { index := &indices[carry-1] if *index+1 < len(context.parentSelectorsNoPseudo) { *index++ break } *index = 0 carry-- } if carry == 0 { break } } r.Selectors = selectors } // Put limits on the combinatorial explosion to avoid using too much time and/or memory if n := len(r.Selectors); n > oldSelectorsLen && n > 0xFF00 { p.addExpansionError(rule.Loc, n) return css_ast.Rule{} } if n := complexSelectorTermCount(r.Selectors); n > oldSelectorsComplexity && n > 0xFF00 { p.addExpansionError(rule.Loc, n) return css_ast.Rule{} } // Lower all child rules using our newly substituted selector context.loweredRules = p.lowerNestingInRule(rule, context.loweredRules) return css_ast.Rule{} case *css_ast.RKnownAt: childContext := lowerNestingContext{ parentSelectorsWithPseudo: context.parentSelectorsWithPseudo, parentSelectorsNoPseudo: context.parentSelectorsNoPseudo, } r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext) // "div { @supports (color: red) { color: red } }" "@supports (color: red) { div { color: red } }" if len(r.Rules) > 0 { childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{ Selectors: context.parentSelectorsWithPseudo, Rules: r.Rules, }}}, childContext.loweredRules...) } // "div { @supports (color: red) { &:hover { color: red } } }" "@supports (color: red) { div:hover { color: red } }" if len(childContext.loweredRules) > 0 { r.Rules = childContext.loweredRules context.loweredRules = append(context.loweredRules, rule) } return css_ast.Rule{} case *css_ast.RAtMedia: childContext := lowerNestingContext{ parentSelectorsWithPseudo: context.parentSelectorsWithPseudo, parentSelectorsNoPseudo: context.parentSelectorsNoPseudo, } r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext) // "div { @media screen { color: red } }" "@media screen { div { color: red } }" if len(r.Rules) > 0 { childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{ Selectors: context.parentSelectorsWithPseudo, Rules: r.Rules, }}}, childContext.loweredRules...) } // "div { @media screen { &:hover { color: red } } }" "@media screen { div:hover { color: red } }" if len(childContext.loweredRules) > 0 { r.Rules = childContext.loweredRules context.loweredRules = append(context.loweredRules, rule) } return css_ast.Rule{} case *css_ast.RAtLayer: // Lower all children and filter out ones that become empty childContext := lowerNestingContext{ parentSelectorsWithPseudo: context.parentSelectorsWithPseudo, parentSelectorsNoPseudo: context.parentSelectorsNoPseudo, } r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext) // "div { @layer foo { color: red } }" "@layer foo { div { color: red } }" if len(r.Rules) > 0 { childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{ Selectors: context.parentSelectorsWithPseudo, Rules: r.Rules, }}}, childContext.loweredRules...) } // "div { @layer foo { &:hover { color: red } } }" "@layer foo { div:hover { color: red } }" // "div { @layer foo {} }" => "@layer foo {}" (layers have side effects, so don't remove empty ones) r.Rules = childContext.loweredRules context.loweredRules = append(context.loweredRules, rule) return css_ast.Rule{} } return rule } type leadingCombinatorStrip uint8 const ( keepLeadingCombinator leadingCombinatorStrip = iota stripLeadingCombinator ) func (p *parser) substituteAmpersandsInCompoundSelector( sel css_ast.CompoundSelector, replacementFn func(logger.Loc) css_ast.ComplexSelector, results []css_ast.CompoundSelector, strip leadingCombinatorStrip, ) []css_ast.CompoundSelector { for _, nestingSelectorLoc := range sel.NestingSelectorLocs { replacement := replacementFn(nestingSelectorLoc) // Convert the replacement to a single compound selector var single css_ast.CompoundSelector if sel.Combinator.Byte == 0 && (len(replacement.Selectors) == 1 || len(results) == 0) { // ".foo { :hover & {} }" => ":hover .foo {}" // ".foo .bar { &:hover {} }" => ".foo .bar:hover {}" last := len(replacement.Selectors) - 1 results = append(results, replacement.Selectors[:last]...) single = replacement.Selectors[last] if strip == stripLeadingCombinator { single.Combinator = css_ast.Combinator{} } sel.Combinator = single.Combinator } else if len(replacement.Selectors) == 1 { // ".foo { > &:hover {} }" => ".foo > .foo:hover {}" single = replacement.Selectors[0] if strip == stripLeadingCombinator { single.Combinator = css_ast.Combinator{} } } else { // ".foo .bar { :hover & {} }" => ":hover :is(.foo .bar) {}" // ".foo .bar { > &:hover {} }" => ".foo .bar > :is(.foo .bar):hover {}" p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc) single = css_ast.CompoundSelector{ SubclassSelectors: []css_ast.SubclassSelector{{ Range: logger.Range{Loc: nestingSelectorLoc}, Data: &css_ast.SSPseudoClassWithSelectorList{ Kind: css_ast.PseudoClassIs, Selectors: []css_ast.ComplexSelector{replacement.Clone()}, }, }}, } } var subclassSelectorPrefix []css_ast.SubclassSelector // Insert the type selector if single.TypeSelector != nil { if sel.TypeSelector != nil { p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc) subclassSelectorPrefix = append(subclassSelectorPrefix, css_ast.SubclassSelector{ Range: sel.TypeSelector.Range(), Data: &css_ast.SSPseudoClassWithSelectorList{ Kind: css_ast.PseudoClassIs, Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: sel.TypeSelector}}}}, }, }) } sel.TypeSelector = single.TypeSelector } // Insert the subclass selectors subclassSelectorPrefix = append(subclassSelectorPrefix, single.SubclassSelectors...) // Write the changes back if len(subclassSelectorPrefix) > 0 { sel.SubclassSelectors = append(subclassSelectorPrefix, sel.SubclassSelectors...) } } sel.NestingSelectorLocs = nil // "div { :is(&.foo) {} }" => ":is(div.foo) {}" for _, ss := range sel.SubclassSelectors { if class, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok { outer := make([]css_ast.ComplexSelector, 0, len(class.Selectors)) for _, complex := range class.Selectors { inner := make([]css_ast.CompoundSelector, 0, len(complex.Selectors)) for _, sel := range complex.Selectors { inner = p.substituteAmpersandsInCompoundSelector(sel, replacementFn, inner, stripLeadingCombinator) } outer = append(outer, css_ast.ComplexSelector{Selectors: inner}) } class.Selectors = outer } } return append(results, sel) } // Turn the list of selectors into a single selector by wrapping lists // without a single element with ":is(...)". Note that this may result // in an empty ":is()" selector (which matches nothing). func (p *parser) multipleComplexSelectorsToSingleComplexSelector(selectors []css_ast.ComplexSelector) func(logger.Loc) css_ast.ComplexSelector { if len(selectors) == 1 { return func(logger.Loc) css_ast.ComplexSelector { return selectors[0] } } var leadingCombinator css_ast.Combinator clones := make([]css_ast.ComplexSelector, len(selectors)) for i, sel := range selectors { // "> a, > b" => "> :is(a, b)" (the caller should have already checked that all leading combinators are the same) leadingCombinator = sel.Selectors[0].Combinator clones[i] = sel.Clone() } return func(loc logger.Loc) css_ast.ComplexSelector { return css_ast.ComplexSelector{ Selectors: []css_ast.CompoundSelector{{ Combinator: leadingCombinator, SubclassSelectors: []css_ast.SubclassSelector{{ Range: logger.Range{Loc: loc}, Data: &css_ast.SSPseudoClassWithSelectorList{ Kind: css_ast.PseudoClassIs, Selectors: clones, }, }}, }}, } } } func (p *parser) reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc logger.Loc) { if p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) { _, didWarn := p.nestingWarnings[nestingSelectorLoc] if didWarn { // Only warn at each location once return } if p.nestingWarnings == nil { p.nestingWarnings = make(map[logger.Loc]struct{}) } p.nestingWarnings[nestingSelectorLoc] = struct{}{} text := "Transforming this CSS nesting syntax is not supported in the configured target environment" if p.options.originalTargetEnv != "" { text = fmt.Sprintf("%s (%s)", text, p.options.originalTargetEnv) } r := logger.Range{Loc: nestingSelectorLoc, Len: 1} p.log.AddIDWithNotes(logger.MsgID_CSS_UnsupportedCSSNesting, logger.Warning, &p.tracker, r, text, []logger.MsgData{{ Text: "The nesting transform for this case must generate an \":is(...)\" but the configured target environment does not support the \":is\" pseudo-class."}}) } }