Skip to content

Commit

Permalink
css: support An+B and :nth-*() syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 26, 2023
1 parent 517fd5c commit 63de9e5
Show file tree
Hide file tree
Showing 7 changed files with 541 additions and 36 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

## Unreleased

* Support `An+B` syntax and `:nth-*()` pseudo-classes in CSS

This adds support for the `:nth-child()`, `:nth-last-child()`, `:nth-of-type()`, and `:nth-last-of-type()` pseudo-classes to esbuild, which has the following consequences:

* The [`An+B` syntax](https://drafts.csswg.org/css-syntax-3/#anb-microsyntax) is now parsed, so parse errors are now reported
* `An+B` values inside these pseudo-classes are now pretty-printed (e.g. a leading `+` will be stripped because it's not in the AST)
* When minification is enabled, `An+B` values are reduced to equivalent but shorter forms (e.g. `2n+0` => `2n`, `2n+1` => `odd`)
* Local CSS names in an `of` clause are now detected (e.g. in `:nth-child(2n of :local(.foo))` the name `foo` is now renamed)

```css
/* Original code */
.foo:nth-child(+2n+1 of :local(.bar)) {
color: red;
}

/* Old output (with --loader=local-css) */
.stdin_foo:nth-child(+2n + 1 of :local(.bar)) {
color: red;
}

/* New output (with --loader=local-css) */
.stdin_foo:nth-child(2n+1 of .stdin_bar) {
color: red;
}
```

* Adjust CSS nesting parser for IE7 hacks ([#3272](https://github.com/evanw/esbuild/issues/3272))

This fixes a regression with esbuild's treatment of IE7 hacks in CSS. CSS nesting allows selectors to be used where declarations are expected. There's an IE7 hack where prefixing a declaration with a `*` causes that declaration to only be applied in IE7 due to a bug in IE7's CSS parser. However, it's valid for nested CSS selectors to start with `*`. So esbuild was incorrectly parsing these declarations and anything following it up until the next `{` as a selector for a nested CSS rule. This release changes esbuild's parser to terminate the parsing of selectors for nested CSS rules when a `;` is encountered to fix this edge case:
Expand Down
60 changes: 45 additions & 15 deletions internal/bundler_tests/bundler_css_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,32 +382,62 @@ func TestImportCSSFromJSLocalVsGlobal(t *testing.T) {
}

func TestImportCSSFromJSLowerBareLocalAndGlobal(t *testing.T) {
css := `
.before { color: #000 }
:local { .button { color: #000 } }
.after { color: #000 }
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
.before { color: #000 }
:local { .button { color: #000 } }
.after { color: #000 }
.before { color: #001 }
:global { .button { color: #001 } }
.after { color: #001 }
.before { color: #001 }
:global { .button { color: #001 } }
.after { color: #001 }
div { :local { .button { color: #002 } } }
div { :global { .button { color: #003 } } }
div { :local { .button { color: #002 } } }
div { :global { .button { color: #003 } } }
:local(:global) { color: #004 }
:global(:local) { color: #005 }
:local(:global) { color: #004 }
:global(:local) { color: #005 }
:local(:global) { .button { color: #006 } }
:global(:local) { .button { color: #007 } }
`
:local(:global) { .button { color: #006 } }
:global(:local) { .button { color: #007 } }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}

func TestImportCSSFromJSNthIndexLocal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": css,
"/styles.css": `
:nth-child(2n of .local) { color: #000 }
:nth-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
:nth-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
.local1, :nth-child(2n of :global .GLOBAL), .local2 { color: #003 }
:nth-last-child(2n of .local) { color: #000 }
:nth-last-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
:nth-last-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
.local1, :nth-last-child(2n of :global .GLOBAL), .local2 { color: #003 }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Expand Down
44 changes: 44 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_css.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,50 @@ div .button {
color: #007;
}

================================================================================
TestImportCSSFromJSNthIndexLocal
---------- /out/entry.js ----------
// styles.css
var styles_default = {
local: "styles_local",
local1: "styles_local1",
local2: "styles_local2"
};

// entry.js
console.log(styles_default);

---------- /out/entry.css ----------
/* styles.css */
:nth-child(2n of .styles_local) {
color: #000;
}
:nth-child(2n of #styles_local, .GLOBAL) {
color: #001;
}
:nth-child(2n of .styles_local1 .GLOBAL1, .GLOBAL2 .styles_local2) {
color: #002;
}
.styles_local1,
:nth-child(2n of .GLOBAL),
.styles_local2 {
color: #003;
}
:nth-last-child(2n of .styles_local) {
color: #000;
}
:nth-last-child(2n of #styles_local, .GLOBAL) {
color: #001;
}
:nth-last-child(2n of .styles_local1 .GLOBAL1, .GLOBAL2 .styles_local2) {
color: #002;
}
.styles_local1,
:nth-last-child(2n of .GLOBAL),
.styles_local2 {
color: #003;
}

================================================================================
TestImportGlobalCSSFromJS
---------- /out/entry.js ----------
Expand Down
60 changes: 58 additions & 2 deletions internal/css_ast/css_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,9 +936,17 @@ const (
PseudoClassIs
PseudoClassLocal
PseudoClassNot
PseudoClassNthChild
PseudoClassNthLastChild
PseudoClassNthLastOfType
PseudoClassNthOfType
PseudoClassWhere
)

func (kind PseudoClassKind) HasNthIndex() bool {
return kind >= PseudoClassNthChild && kind <= PseudoClassNthOfType
}

func (kind PseudoClassKind) String() string {
switch kind {
case PseudoClassGlobal:
Expand All @@ -951,27 +959,75 @@ func (kind PseudoClassKind) String() string {
return "local"
case PseudoClassNot:
return "not"
case PseudoClassNthChild:
return "nth-child"
case PseudoClassNthLastChild:
return "nth-last-child"
case PseudoClassNthLastOfType:
return "nth-last-of-type"
case PseudoClassNthOfType:
return "nth-of-type"
case PseudoClassWhere:
return "where"
default:
panic("Internal error")
}
}

// This is the "An+B" syntax
type NthIndex struct {
A string
B string // May be "even" or "odd"
}

func (index *NthIndex) Minify() {
// "even" => "2n"
if index.B == "even" {
index.A = "2"
index.B = ""
return
}

// "2n+1" => "odd"
if index.A == "2" && index.B == "1" {
index.A = ""
index.B = "odd"
return
}

// "0n+1" => "1"
if index.A == "0" {
index.A = ""
if index.B == "" {
// "0n" => "0"
index.B = "0"
}
return
}

// "1n+0" => "1n"
if index.B == "0" && index.A != "" {
index.B = ""
}
}

// See https://drafts.csswg.org/selectors/#grouping
type SSPseudoClassWithSelectorList struct {
Kind PseudoClassKind
Selectors []ComplexSelector
Index NthIndex
Kind PseudoClassKind
}

func (a *SSPseudoClassWithSelectorList) Equal(ss SS, check *CrossFileEqualityCheck) bool {
b, ok := ss.(*SSPseudoClassWithSelectorList)
return ok && a.Kind == b.Kind && ComplexSelectorsEqual(a.Selectors, b.Selectors, check)
return ok && a.Kind == b.Kind && a.Index == b.Index && ComplexSelectorsEqual(a.Selectors, b.Selectors, check)
}

func (ss *SSPseudoClassWithSelectorList) Hash() uint32 {
hash := uint32(5)
hash = helpers.HashCombine(hash, uint32(ss.Kind))
hash = helpers.HashCombineString(hash, ss.Index.A)
hash = helpers.HashCombineString(hash, ss.Index.B)
hash = HashComplexSelectors(hash, ss.Selectors)
return hash
}
Expand Down

0 comments on commit 63de9e5

Please sign in to comment.