Files

512 lines
18 KiB
Go

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."}})
}
}