Skip to content

Commit 96e09b4

Browse files
committed
cannot inline no-op nesting with pseudo-elements
1 parent cd62fa1 commit 96e09b4

File tree

3 files changed

+95
-25
lines changed

3 files changed

+95
-25
lines changed

internal/css_ast/css_ast.go

+23
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,29 @@ type ComplexSelector struct {
598598
Selectors []CompoundSelector
599599
}
600600

601+
func (sel ComplexSelector) UsesPseudoElement() bool {
602+
for _, sel := range sel.Selectors {
603+
for _, sub := range sel.SubclassSelectors {
604+
if class, ok := sub.(*SSPseudoClass); ok {
605+
if class.IsElement {
606+
return true
607+
}
608+
609+
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
610+
// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
611+
// and ::first-letter) may, for legacy reasons, be represented using
612+
// the <pseudo-class-selector> grammar, with only a single ":"
613+
// character at their start.
614+
switch class.Name {
615+
case "before", "after", "first-line", "first-letter":
616+
return true
617+
}
618+
}
619+
}
620+
}
621+
return false
622+
}
623+
601624
func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck) bool {
602625
if len(a.Selectors) != len(b.Selectors) {
603626
return false

internal/css_parser/css_parser.go

+56-25
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,11 @@ loop:
295295
return rules
296296
}
297297

298-
func (p *parser) parseListOfDeclarations() (list []css_ast.Rule) {
298+
type listOfDeclarationsOpts struct {
299+
canInlineNoOpNesting bool
300+
}
301+
302+
func (p *parser) parseListOfDeclarations(opts listOfDeclarationsOpts) (list []css_ast.Rule) {
299303
list = []css_ast.Rule{}
300304
foundNesting := false
301305

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

312316
// Pull out all unnecessarily-nested declarations and stick them at the end
313-
// "a { & { b: c } d: e }" => "a { d: e; b: c; }"
314-
if foundNesting {
315-
var inlineDecls []css_ast.Rule
316-
n := 0
317-
for _, rule := range list {
318-
if rule, ok := rule.Data.(*css_ast.RSelector); ok && len(rule.Selectors) == 1 {
319-
if sel := rule.Selectors[0]; len(sel.Selectors) == 1 && sel.Selectors[0].IsSingleAmpersand() {
320-
inlineDecls = append(inlineDecls, rule.Rules...)
321-
continue
317+
if opts.canInlineNoOpNesting {
318+
// "a { & { x: y } }" => "a { x: y }"
319+
// "a { & { b: c } d: e }" => "a { d: e; b: c }"
320+
if foundNesting {
321+
var inlineDecls []css_ast.Rule
322+
n := 0
323+
for _, rule := range list {
324+
if rule, ok := rule.Data.(*css_ast.RSelector); ok && len(rule.Selectors) == 1 {
325+
if sel := rule.Selectors[0]; len(sel.Selectors) == 1 && sel.Selectors[0].IsSingleAmpersand() {
326+
inlineDecls = append(inlineDecls, rule.Rules...)
327+
continue
328+
}
322329
}
330+
list[n] = rule
331+
n++
323332
}
324-
list[n] = rule
325-
n++
333+
list = append(list[:n], inlineDecls...)
326334
}
327-
list = append(list[:n], inlineDecls...)
335+
} else {
336+
// "a, b::before { & { x: y } }" => "a, b::before { & { x: y } }"
328337
}
329338
}
330339
return
331340

332341
case css_lexer.TAtKeyword:
333342
p.maybeWarnAboutNesting(p.current().Range)
334343
list = append(list, p.parseAtRule(atRuleContext{
335-
isDeclarationList: true,
344+
isDeclarationList: true,
345+
canInlineNoOpNesting: opts.canInlineNoOpNesting,
336346
}))
337347

338348
// Reference: https://drafts.csswg.org/css-nesting-1/
@@ -848,11 +858,12 @@ const (
848858
)
849859

850860
type atRuleContext struct {
851-
afterLoc logger.Loc
852-
charsetValidity atRuleValidity
853-
importValidity atRuleValidity
854-
isDeclarationList bool
855-
isTopLevel bool
861+
afterLoc logger.Loc
862+
charsetValidity atRuleValidity
863+
importValidity atRuleValidity
864+
canInlineNoOpNesting bool
865+
isDeclarationList bool
866+
isTopLevel bool
856867
}
857868

858869
func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
@@ -1006,7 +1017,7 @@ abortRuleParser:
10061017
case css_lexer.TOpenBrace:
10071018
blockMatchingLoc := p.current().Range.Loc
10081019
p.advance()
1009-
rules := p.parseListOfDeclarations()
1020+
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
10101021
p.expectWithMatchingLoc(css_lexer.TCloseBrace, blockMatchingLoc)
10111022

10121023
// "@keyframes { from {} to { color: red } }" => "@keyframes { to { color: red } }"
@@ -1108,7 +1119,9 @@ abortRuleParser:
11081119
if len(names) <= 1 && p.eat(css_lexer.TOpenBrace) {
11091120
var rules []css_ast.Rule
11101121
if context.isDeclarationList {
1111-
rules = p.parseListOfDeclarations()
1122+
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
1123+
canInlineNoOpNesting: context.canInlineNoOpNesting,
1124+
})
11121125
} else {
11131126
rules = p.parseListOfRules(ruleContext{
11141127
parseSelectors: true,
@@ -1210,7 +1223,7 @@ prelude:
12101223
// Parse known rules whose blocks always consist of declarations
12111224
matchingLoc := p.current().Range.Loc
12121225
p.expect(css_lexer.TOpenBrace)
1213-
rules := p.parseListOfDeclarations()
1226+
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
12141227
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
12151228
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}}
12161229

@@ -1220,7 +1233,9 @@ prelude:
12201233
p.expect(css_lexer.TOpenBrace)
12211234
var rules []css_ast.Rule
12221235
if context.isDeclarationList {
1223-
rules = p.parseListOfDeclarations()
1236+
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
1237+
canInlineNoOpNesting: context.canInlineNoOpNesting,
1238+
})
12241239
} else {
12251240
rules = p.parseListOfRules(ruleContext{
12261241
parseSelectors: true,
@@ -1624,10 +1639,26 @@ func mangleNumber(t string) (string, bool) {
16241639
func (p *parser) parseSelectorRuleFrom(preludeStart int, isTopLevel bool, opts parseSelectorOpts) css_ast.Rule {
16251640
// Try parsing the prelude as a selector list
16261641
if list, ok := p.parseSelectorList(opts); ok {
1642+
canInlineNoOpNesting := true
1643+
for _, sel := range list {
1644+
// We cannot transform the CSS "a, b::before { & { color: red } }" into
1645+
// "a, b::before { color: red }" because it's basically equivalent to
1646+
// ":is(a, b::before) { color: red }" which only applies to "a", not to
1647+
// "b::before" because pseudo-elements are not valid within :is():
1648+
// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
1649+
// may be relaxed in the future, but this restriction hash shipped so
1650+
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
1651+
if sel.UsesPseudoElement() {
1652+
canInlineNoOpNesting = false
1653+
break
1654+
}
1655+
}
16271656
selector := css_ast.RSelector{Selectors: list}
16281657
matchingLoc := p.current().Range.Loc
16291658
if p.expect(css_lexer.TOpenBrace) {
1630-
selector.Rules = p.parseListOfDeclarations()
1659+
selector.Rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
1660+
canInlineNoOpNesting: canInlineNoOpNesting,
1661+
})
16311662
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
16321663
return css_ast.Rule{Loc: p.tokens[preludeStart].Range.Loc, Data: &selector}
16331664
}
@@ -1671,7 +1702,7 @@ loop:
16711702

16721703
matchingLoc := p.current().Range.Loc
16731704
if p.eat(css_lexer.TOpenBrace) {
1674-
qualified.Rules = p.parseListOfDeclarations()
1705+
qualified.Rules = p.parseListOfDeclarations(listOfDeclarationsOpts{})
16751706
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
16761707
} else if !opts.isAlreadyInvalid {
16771708
p.expect(css_lexer.TOpenBrace)

internal/css_parser/css_parser_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,22 @@ func TestNestedSelector(t *testing.T) {
838838
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")
839839
expectPrintedMangle(t, "div { .b { x: 1 } & { x: 2 } }", "div {\n .b {\n x: 1;\n }\n x: 2;\n}\n")
840840
expectPrintedMangle(t, "div { & { & { & { color: red } } & { & { zoom: 2 } } } }", "div {\n color: red;\n zoom: 2;\n}\n")
841+
842+
// Cannot inline no-op nesting with pseudo-elements (https://github.com/w3c/csswg-drafts/issues/7433)
843+
expectPrintedMangle(t, "div, span:hover { & { color: red } }", "div,\nspan:hover {\n color: red;\n}\n")
844+
expectPrintedMangle(t, "div, span::before { & { color: red } }", "div,\nspan:before {\n & {\n color: red;\n }\n}\n")
845+
expectPrintedMangle(t, "div, span:before { & { color: red } }", "div,\nspan:before {\n & {\n color: red;\n }\n}\n")
846+
expectPrintedMangle(t, "div, span::after { & { color: red } }", "div,\nspan:after {\n & {\n color: red;\n }\n}\n")
847+
expectPrintedMangle(t, "div, span:after { & { color: red } }", "div,\nspan:after {\n & {\n color: red;\n }\n}\n")
848+
expectPrintedMangle(t, "div, span::first-line { & { color: red } }", "div,\nspan:first-line {\n & {\n color: red;\n }\n}\n")
849+
expectPrintedMangle(t, "div, span:first-line { & { color: red } }", "div,\nspan:first-line {\n & {\n color: red;\n }\n}\n")
850+
expectPrintedMangle(t, "div, span::first-letter { & { color: red } }", "div,\nspan:first-letter {\n & {\n color: red;\n }\n}\n")
851+
expectPrintedMangle(t, "div, span:first-letter { & { color: red } }", "div,\nspan:first-letter {\n & {\n color: red;\n }\n}\n")
852+
expectPrintedMangle(t, "div, span::pseudo { & { color: red } }", "div,\nspan::pseudo {\n & {\n color: red;\n }\n}\n")
853+
expectPrintedMangle(t, "div, span:hover { @layer foo { & { color: red } } }", "div,\nspan:hover {\n @layer foo {\n color: red;\n }\n}\n")
854+
expectPrintedMangle(t, "div, span:hover { @media screen { & { color: red } } }", "div,\nspan:hover {\n @media screen {\n color: red;\n }\n}\n")
855+
expectPrintedMangle(t, "div, span::pseudo { @layer foo { & { color: red } } }", "div,\nspan::pseudo {\n @layer foo {\n & {\n color: red;\n }\n }\n}\n")
856+
expectPrintedMangle(t, "div, span::pseudo { @media screen { & { color: red } } }", "div,\nspan::pseudo {\n @media screen {\n & {\n color: red;\n }\n }\n}\n")
841857
}
842858

843859
func TestBadQualifiedRules(t *testing.T) {

0 commit comments

Comments
 (0)