Skip to content

Commit

Permalink
cannot inline no-op nesting with pseudo-elements
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 25, 2023
1 parent cd62fa1 commit 96e09b4
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 25 deletions.
23 changes: 23 additions & 0 deletions internal/css_ast/css_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,29 @@ type ComplexSelector struct {
Selectors []CompoundSelector
}

func (sel ComplexSelector) UsesPseudoElement() bool {
for _, sel := range sel.Selectors {
for _, sub := range sel.SubclassSelectors {
if class, ok := sub.(*SSPseudoClass); ok {
if class.IsElement {
return true
}

// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
// and ::first-letter) may, for legacy reasons, be represented using
// the <pseudo-class-selector> grammar, with only a single ":"
// character at their start.
switch class.Name {
case "before", "after", "first-line", "first-letter":
return true
}
}
}
}
return false
}

func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck) bool {
if len(a.Selectors) != len(b.Selectors) {
return false
Expand Down
81 changes: 56 additions & 25 deletions internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,11 @@ loop:
return rules
}

func (p *parser) parseListOfDeclarations() (list []css_ast.Rule) {
type listOfDeclarationsOpts struct {
canInlineNoOpNesting bool
}

func (p *parser) parseListOfDeclarations(opts listOfDeclarationsOpts) (list []css_ast.Rule) {
list = []css_ast.Rule{}
foundNesting := false

Expand All @@ -310,29 +314,35 @@ func (p *parser) parseListOfDeclarations() (list []css_ast.Rule) {
list = p.mangleRules(list, false /* isTopLevel */)

// Pull out all unnecessarily-nested declarations and stick them at the end
// "a { & { b: c } d: e }" => "a { d: e; b: c; }"
if foundNesting {
var inlineDecls []css_ast.Rule
n := 0
for _, rule := range list {
if rule, ok := rule.Data.(*css_ast.RSelector); ok && len(rule.Selectors) == 1 {
if sel := rule.Selectors[0]; len(sel.Selectors) == 1 && sel.Selectors[0].IsSingleAmpersand() {
inlineDecls = append(inlineDecls, rule.Rules...)
continue
if opts.canInlineNoOpNesting {
// "a { & { x: y } }" => "a { x: y }"
// "a { & { b: c } d: e }" => "a { d: e; b: c }"
if foundNesting {
var inlineDecls []css_ast.Rule
n := 0
for _, rule := range list {
if rule, ok := rule.Data.(*css_ast.RSelector); ok && len(rule.Selectors) == 1 {
if sel := rule.Selectors[0]; len(sel.Selectors) == 1 && sel.Selectors[0].IsSingleAmpersand() {
inlineDecls = append(inlineDecls, rule.Rules...)
continue
}
}
list[n] = rule
n++
}
list[n] = rule
n++
list = append(list[:n], inlineDecls...)
}
list = append(list[:n], inlineDecls...)
} else {
// "a, b::before { & { x: y } }" => "a, b::before { & { x: y } }"
}
}
return

case css_lexer.TAtKeyword:
p.maybeWarnAboutNesting(p.current().Range)
list = append(list, p.parseAtRule(atRuleContext{
isDeclarationList: true,
isDeclarationList: true,
canInlineNoOpNesting: opts.canInlineNoOpNesting,
}))

// Reference: https://drafts.csswg.org/css-nesting-1/
Expand Down Expand Up @@ -848,11 +858,12 @@ const (
)

type atRuleContext struct {
afterLoc logger.Loc
charsetValidity atRuleValidity
importValidity atRuleValidity
isDeclarationList bool
isTopLevel bool
afterLoc logger.Loc
charsetValidity atRuleValidity
importValidity atRuleValidity
canInlineNoOpNesting bool
isDeclarationList bool
isTopLevel bool
}

func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
Expand Down Expand Up @@ -1006,7 +1017,7 @@ abortRuleParser:
case css_lexer.TOpenBrace:
blockMatchingLoc := p.current().Range.Loc
p.advance()
rules := p.parseListOfDeclarations()
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
p.expectWithMatchingLoc(css_lexer.TCloseBrace, blockMatchingLoc)

// "@keyframes { from {} to { color: red } }" => "@keyframes { to { color: red } }"
Expand Down Expand Up @@ -1108,7 +1119,9 @@ abortRuleParser:
if len(names) <= 1 && p.eat(css_lexer.TOpenBrace) {
var rules []css_ast.Rule
if context.isDeclarationList {
rules = p.parseListOfDeclarations()
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
canInlineNoOpNesting: context.canInlineNoOpNesting,
})
} else {
rules = p.parseListOfRules(ruleContext{
parseSelectors: true,
Expand Down Expand Up @@ -1210,7 +1223,7 @@ prelude:
// Parse known rules whose blocks always consist of declarations
matchingLoc := p.current().Range.Loc
p.expect(css_lexer.TOpenBrace)
rules := p.parseListOfDeclarations()
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}}

Expand All @@ -1220,7 +1233,9 @@ prelude:
p.expect(css_lexer.TOpenBrace)
var rules []css_ast.Rule
if context.isDeclarationList {
rules = p.parseListOfDeclarations()
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
canInlineNoOpNesting: context.canInlineNoOpNesting,
})
} else {
rules = p.parseListOfRules(ruleContext{
parseSelectors: true,
Expand Down Expand Up @@ -1624,10 +1639,26 @@ func mangleNumber(t string) (string, bool) {
func (p *parser) parseSelectorRuleFrom(preludeStart int, isTopLevel bool, opts parseSelectorOpts) css_ast.Rule {
// Try parsing the prelude as a selector list
if list, ok := p.parseSelectorList(opts); ok {
canInlineNoOpNesting := true
for _, sel := range list {
// We cannot transform the CSS "a, b::before { & { color: red } }" into
// "a, b::before { color: red }" because it's basically equivalent to
// ":is(a, b::before) { color: red }" which only applies to "a", not to
// "b::before" 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 hash shipped so
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
if sel.UsesPseudoElement() {
canInlineNoOpNesting = false
break
}
}
selector := css_ast.RSelector{Selectors: list}
matchingLoc := p.current().Range.Loc
if p.expect(css_lexer.TOpenBrace) {
selector.Rules = p.parseListOfDeclarations()
selector.Rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
canInlineNoOpNesting: canInlineNoOpNesting,
})
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
return css_ast.Rule{Loc: p.tokens[preludeStart].Range.Loc, Data: &selector}
}
Expand Down Expand Up @@ -1671,7 +1702,7 @@ loop:

matchingLoc := p.current().Range.Loc
if p.eat(css_lexer.TOpenBrace) {
qualified.Rules = p.parseListOfDeclarations()
qualified.Rules = p.parseListOfDeclarations(listOfDeclarationsOpts{})
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
} else if !opts.isAlreadyInvalid {
p.expect(css_lexer.TOpenBrace)
Expand Down
16 changes: 16 additions & 0 deletions internal/css_parser/css_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,22 @@ func TestNestedSelector(t *testing.T) {
expectPrintedMangle(t, "div { a: 1; & { b: 4 } b: 2; && { c: 5 } c: 3 }", "div {\n a: 1;\n b: 2;\n c: 3;\n b: 4;\n c: 5;\n}\n")
expectPrintedMangle(t, "div { .b { x: 1 } & { x: 2 } }", "div {\n .b {\n x: 1;\n }\n x: 2;\n}\n")
expectPrintedMangle(t, "div { & { & { & { color: red } } & { & { zoom: 2 } } } }", "div {\n color: red;\n zoom: 2;\n}\n")

// Cannot inline no-op nesting with pseudo-elements (https://github.com/w3c/csswg-drafts/issues/7433)
expectPrintedMangle(t, "div, span:hover { & { color: red } }", "div,\nspan:hover {\n color: red;\n}\n")
expectPrintedMangle(t, "div, span::before { & { color: red } }", "div,\nspan:before {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:before { & { color: red } }", "div,\nspan:before {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span::after { & { color: red } }", "div,\nspan:after {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:after { & { color: red } }", "div,\nspan:after {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span::first-line { & { color: red } }", "div,\nspan:first-line {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:first-line { & { color: red } }", "div,\nspan:first-line {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span::first-letter { & { color: red } }", "div,\nspan:first-letter {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:first-letter { & { color: red } }", "div,\nspan:first-letter {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span::pseudo { & { color: red } }", "div,\nspan::pseudo {\n & {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:hover { @layer foo { & { color: red } } }", "div,\nspan:hover {\n @layer foo {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span:hover { @media screen { & { color: red } } }", "div,\nspan:hover {\n @media screen {\n color: red;\n }\n}\n")
expectPrintedMangle(t, "div, span::pseudo { @layer foo { & { color: red } } }", "div,\nspan::pseudo {\n @layer foo {\n & {\n color: red;\n }\n }\n}\n")
expectPrintedMangle(t, "div, span::pseudo { @media screen { & { color: red } } }", "div,\nspan::pseudo {\n @media screen {\n & {\n color: red;\n }\n }\n}\n")
}

func TestBadQualifiedRules(t *testing.T) {
Expand Down

0 comments on commit 96e09b4

Please sign in to comment.