Skip to content

Commit

Permalink
allow trailing "&" nesting selectors without space
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 20, 2022
1 parent 69996b4 commit 11ed34e
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 21 deletions.
14 changes: 11 additions & 3 deletions internal/css_ast/css_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ type RSelector struct {

func (a *RSelector) Equal(rule R) bool {
b, ok := rule.(*RSelector)
if ok && len(a.Selectors) == len(b.Selectors) {
if ok && len(a.Selectors) == len(b.Selectors) && a.HasAtNest == b.HasAtNest {
for i, sel := range a.Selectors {
if !sel.Equal(b.Selectors[i]) {
return false
Expand Down Expand Up @@ -524,7 +524,7 @@ func (a ComplexSelector) Equal(b ComplexSelector) bool {

for i, ai := range a.Selectors {
bi := b.Selectors[i]
if ai.HasNestPrefix != bi.HasNestPrefix || ai.Combinator != bi.Combinator {
if ai.NestingSelector != bi.NestingSelector || ai.Combinator != bi.Combinator {
return false
}

Expand All @@ -547,11 +547,19 @@ func (a ComplexSelector) Equal(b ComplexSelector) bool {
return true
}

type NestingSelector uint8

const (
NestingSelectorNone NestingSelector = iota
NestingSelectorPrefix // "&a {}"
NestingSelectorPresentButNotPrefix // "a& {}"
)

type CompoundSelector struct {
Combinator string // Optional, may be ""
TypeSelector *NamespacedName
SubclassSelectors []SS
HasNestPrefix bool // "&"
NestingSelector NestingSelector // "&"
}

type NameToken struct {
Expand Down
4 changes: 2 additions & 2 deletions internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ var nonDeprecatedElementsSupportedByIE7 = map[string]bool{
func isSafeSelectors(complexSelectors []css_ast.ComplexSelector) bool {
for _, complex := range complexSelectors {
for _, compound := range complex.Selectors {
if compound.HasNestPrefix {
if compound.NestingSelector != css_ast.NestingSelectorNone {
// Bail because this is an extension: https://drafts.csswg.org/css-nesting-1/
return false
}
Expand Down Expand Up @@ -1243,7 +1243,7 @@ func (p *parser) parseSelectorRuleFrom(preludeStart int, opts parseSelectorOpts)
if p.options.MangleSyntax && selector.HasAtNest {
allHaveNestPrefix := true
for _, complex := range selector.Selectors {
if len(complex.Selectors) == 0 || !complex.Selectors[0].HasNestPrefix {
if len(complex.Selectors) == 0 || complex.Selectors[0].NestingSelector != css_ast.NestingSelectorPrefix {
allHaveNestPrefix = false
break
}
Expand Down
41 changes: 26 additions & 15 deletions internal/css_parser/css_parser_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.Co
if !good {
return
}
hasNestPrefix = sel.HasNestPrefix
hasNestSelector := sel.HasNestPrefix
hasNestPrefix = sel.NestingSelector == css_ast.NestingSelectorPrefix
isNestContaining := sel.NestingSelector != css_ast.NestingSelectorNone
result.Selectors = append(result.Selectors, sel)

for {
Expand All @@ -82,13 +82,13 @@ func (p *parser) parseComplexSelector(opts parseSelectorOpts) (result css_ast.Co
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
if sel.HasNestPrefix {
hasNestSelector = true
if sel.NestingSelector != css_ast.NestingSelectorNone {
isNestContaining = true
}
}

// Validate nest selector consistency
if opts.atNestRange.Len != 0 && !hasNestSelector {
if opts.atNestRange.Len != 0 && !isNestContaining {
p.log.AddWithNotes(logger.Warning, &p.tracker, logger.Range{Loc: loc}, "Every selector in a nested style rule must contain \"&\"",
[]logger.MsgData{p.tracker.MsgData(opts.atNestRange, "This is a nested style rule because of the \"@nest\" here:")})
}
Expand All @@ -104,20 +104,21 @@ func (p *parser) nameToken() css_ast.NameToken {
}
}

func (p *parser) warnNestingUnsupported(r logger.Range) {
text := "CSS nesting syntax is not supported in the configured target environment"
if p.options.OriginalTargetEnv != "" {
text = fmt.Sprintf("%s (%s)", text, p.options.OriginalTargetEnv)
}
p.log.Add(logger.Warning, &p.tracker, r, text)
}

func (p *parser) parseCompoundSelector() (sel css_ast.CompoundSelector, ok bool) {
// This is an extension: https://drafts.csswg.org/css-nesting-1/
r := p.current().Range
if p.eat(css_lexer.TDelimAmpersand) {
sel.HasNestPrefix = true

// Warn if we're targeting a browser, since it won't work
sel.NestingSelector = css_ast.NestingSelectorPrefix
if p.options.UnsupportedCSSFeatures.Has(compat.Nesting) {
where := "the configured target environment"
if p.options.OriginalTargetEnv != "" {
where = fmt.Sprintf("%s (%s)", where, p.options.OriginalTargetEnv)
}
p.log.Add(logger.Warning, &p.tracker, r, fmt.Sprintf(
"CSS nesting syntax is not supported in %s", where))
p.warnNestingUnsupported(r)
}
}

Expand Down Expand Up @@ -201,13 +202,23 @@ subclassSelectors:
pseudo := p.parsePseudoClassSelector()
sel.SubclassSelectors = append(sel.SubclassSelectors, &pseudo)

case css_lexer.TDelimAmpersand:
// This is an extension: https://drafts.csswg.org/css-nesting-1/
r := p.current().Range
if p.eat(css_lexer.TDelimAmpersand) && sel.NestingSelector == css_ast.NestingSelectorNone {
sel.NestingSelector = css_ast.NestingSelectorPresentButNotPrefix
if p.options.UnsupportedCSSFeatures.Has(compat.Nesting) {
p.warnNestingUnsupported(r)
}
}

default:
break subclassSelectors
}
}

// The compound selector must be non-empty
if !sel.HasNestPrefix && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 {
if sel.NestingSelector == css_ast.NestingSelectorNone && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 {
p.unexpected()
return
}
Expand Down
13 changes: 13 additions & 0 deletions internal/css_parser/css_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func expectPrintedLower(t *testing.T, contents string, expected string) {
})
}

func expectPrintedMinify(t *testing.T, contents string, expected string) {
t.Helper()
expectPrintedCommon(t, contents+" [minify]", contents, expected, config.Options{
RemoveWhitespace: true,
})
}

func expectPrintedMangle(t *testing.T, contents string, expected string) {
t.Helper()
expectPrintedCommon(t, contents+" [mangle]", contents, expected, config.Options{
Expand Down Expand Up @@ -703,6 +710,12 @@ func TestNestedSelector(t *testing.T) {
"<stdin>: WARNING: Every selector in a nested style rule must contain \"&\"\n"+
"<stdin>: NOTE: This is a nested style rule because of the \"@nest\" here:\n")
expectPrinted(t, "a { @nest b & { color: red } }", "a {\n @nest b & {\n color: red;\n }\n}\n")
expectPrinted(t, "a { @nest b& { color: red } }", "a {\n @nest b& {\n color: red;\n }\n}\n")
expectPrinted(t, "a { @nest b&[c] { color: red } }", "a {\n @nest b[c]& {\n color: red;\n }\n}\n")
expectPrinted(t, "a { @nest &[c] { color: red } }", "a {\n @nest &[c] {\n color: red;\n }\n}\n")
expectPrinted(t, "a { @nest [c]& { color: red } }", "a {\n @nest [c]& {\n color: red;\n }\n}\n")
expectPrintedMinify(t, "a { @nest b & { color: red } }", "a{@nest b &{color:red}}")
expectPrintedMinify(t, "a { @nest b& { color: red } }", "a{@nest b&{color:red}}")

// Don't drop "@nest" for invalid rules
expectParseError(t, "a { @nest @invalid { color: red } }", "<stdin>: WARNING: Unexpected \"@invalid\"\n")
Expand Down
8 changes: 7 additions & 1 deletion internal/css_printer/css_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo
p.print(" ")
}

if sel.HasNestPrefix {
if sel.NestingSelector == css_ast.NestingSelectorPrefix {
p.print("&")
}

Expand Down Expand Up @@ -378,6 +378,12 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo
p.printPseudoClassSelector(*s, whitespace)
}
}

// It doesn't matter where the "&" goes since all non-prefix cases are
// treated the same. This just always puts it as a suffix for simplicity.
if sel.NestingSelector == css_ast.NestingSelectorPresentButNotPrefix {
p.print("&")
}
}

func (p *printer) printNamespacedName(nsName css_ast.NamespacedName, whitespace trailingWhitespace) {
Expand Down

0 comments on commit 11ed34e

Please sign in to comment.