diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index f56cd765e8d7..52b2dd4c6ad5 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -2480,7 +2480,9 @@ test(
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
- @supports (color: color-mix(in lab, red, red)) {
+ }
+ @supports (color: color-mix(in lab, red, red)) {
+ ::placeholder {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
@@ -2523,7 +2525,9 @@ test(
@layer utilities {
.bg-red-500\\/50 {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent);
- @supports (color: color-mix(in lab, red, red)) {
+ }
+ @supports (color: color-mix(in lab, red, red)) {
+ .bg-red-500\\/50 {
background-color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}
diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts
index 4c936a2a23af..1bb23c1681a8 100644
--- a/integrations/vite/index.test.ts
+++ b/integrations/vite/index.test.ts
@@ -1137,7 +1137,7 @@ test(
- Hello, world!
+ Hello, world!
`,
'src/index.css': css`
@@ -1149,16 +1149,16 @@ test(
async ({ exec, expect, fs }) => {
await exec('pnpm vite build')
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
-
// Should not be minified when optimize is disabled
- let content = await fs.read(filename)
- expect(content).toContain('.hover\\:flex {')
- expect(content).toContain('&:hover {')
- expect(content).toContain('@media (hover: hover) {')
- expect(content).toContain('display: flex;')
+ expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-.css'))
+ .toMatchInlineSnapshot(`
+ "
+ --- dist/assets/index-.css ---
+ .focus\\:text-\\[black\\]:focus {
+ color: black;
+ }
+ "
+ `)
},
)
@@ -1192,7 +1192,7 @@ test(
- Hello, world!
+ Hello, world!
`,
'src/index.css': css`
@@ -1204,15 +1204,17 @@ test(
async ({ exec, expect, fs }) => {
await exec('pnpm vite build')
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
-
- // Should be optimized but not minified
- let content = await fs.read(filename)
- expect(content).toContain('@media (hover: hover) {')
- expect(content).toContain('.hover\\:flex:hover {')
- expect(content).toContain('display: flex;')
+ expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-.css'))
+ .toMatchInlineSnapshot(`
+ "
+ --- dist/assets/index-.css ---
+ @media (hover: hover) {
+ .hover\\:text-\\[black\\]:hover {
+ color: #000;
+ }
+ }
+ "
+ `)
},
)
diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts
index 24f31f7bb5b5..c791d9c8b25a 100644
--- a/packages/tailwindcss/src/ast.test.ts
+++ b/packages/tailwindcss/src/ast.test.ts
@@ -1,26 +1,36 @@
-import { expect, it } from 'vitest'
+import { describe, expect, it, test } from 'vitest'
+import * as SelectorParser from '../src/selector-parser'
import {
atRule,
+ cloneAstNode,
context,
cssContext,
decl,
+ DROPPABLE_IF_EMPTY_AT_RULES,
+ handleNesting,
+ HOISTABLE_AT_RULES,
optimizeAst,
+ optimizeSelector,
+ rule,
styleRule,
toCss,
type AstNode,
} from './ast'
import * as CSS from './css-parser'
import { buildDesignSystem } from './design-system'
+import { SourceLocation } from './source-maps/source'
+import { pretty } from './test-utils/run'
import { Theme } from './theme'
+import { segment } from './utils/segment'
import { walk, WalkAction } from './walk'
const css = String.raw
const defaultDesignSystem = buildDesignSystem(new Theme())
it('should pretty print an AST', () => {
- expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'), defaultDesignSystem)))
- .toMatchInlineSnapshot(`
- ".foo {
+ expect(pretty(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))).toMatchInlineSnapshot(`
+ "
+ .foo {
color: red;
&:hover {
color: blue;
@@ -68,47 +78,18 @@ it('allows the placement of context nodes', () => {
expect(blueContext).toEqual({ context: 'a' })
expect(greenContext).toEqual({ context: 'b' })
- expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(`
- ".foo {
+ expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(`
+ "
+ .foo {
color: red;
}
.bar {
color: blue;
- .baz {
- color: green;
- }
- }
- "
- `)
-})
-
-it('should stop walking when returning `WalkAction.Stop`', () => {
- let ast = [
- styleRule('.foo', [styleRule('.nested', [styleRule('.bail', [decl('color', 'red')])])]),
- styleRule('.bar'),
- styleRule('.baz'),
- styleRule('.qux'),
- ]
-
- let seen = new Set()
-
- walk(ast, (node) => {
- if (node.kind === 'rule') {
- seen.add(node.selector)
- }
-
- if (node.kind === 'rule' && node.selector === '.bail') {
- return WalkAction.Stop
}
- })
-
- // We do not want to see `.bar`, `.baz`, or `.qux` because we bailed early
- expect(seen).toMatchInlineSnapshot(`
- Set {
- ".foo",
- ".nested",
- ".bail",
+ .bar .baz {
+ color: green;
}
+ "
`)
})
@@ -154,13 +135,15 @@ it('should not emit empty rules once optimized', () => {
/* Exceptions: */
@charset "UTF-8";
@layer foo, bar, baz;
+ @layer foo, bar, baz; /* Will be deduped */
@custom-media --modern (color), (hover);
@namespace 'http://www.w3.org/1999/xhtml';
@import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap');
`)
- expect(toCss(ast)).toMatchInlineSnapshot(`
- ".foo {
+ expect(pretty(toCss(ast))).toMatchInlineSnapshot(`
+ "
+ .foo {
}
.foo {
.bar {
@@ -187,14 +170,16 @@ it('should not emit empty rules once optimized', () => {
}
@charset "UTF-8";
@layer foo, bar, baz;
+ @layer foo, bar, baz;
@custom-media --modern (color), (hover);
@namespace 'http://www.w3.org/1999/xhtml';
@import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap');
"
`)
- expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(`
- "@charset "UTF-8";
+ expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(`
+ "
+ @charset "UTF-8";
@layer foo, bar, baz;
@custom-media --modern (color), (hover);
@namespace 'http://www.w3.org/1999/xhtml';
@@ -235,8 +220,9 @@ it('should not emit exact duplicate declarations in the same rule', () => {
}
`)
- expect(toCss(ast)).toMatchInlineSnapshot(`
- ".foo {
+ expect(pretty(toCss(ast))).toMatchInlineSnapshot(`
+ "
+ .foo {
color: red;
.bar {
color: green;
@@ -267,23 +253,26 @@ it('should not emit exact duplicate declarations in the same rule', () => {
"
`)
- expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(`
- ".foo {
- .bar {
- color: blue;
- color: green;
- }
+ expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(`
+ "
+ .foo {
color: red;
}
+ .foo .bar {
+ color: blue;
+ color: green;
+ }
.foo {
color: green;
color: blue;
color: red;
background: blue;
- .bar {
- color: blue;
- color: green;
- }
+ }
+ .foo .bar {
+ color: blue;
+ color: green;
+ }
+ .foo {
caret-color: orange;
}
"
@@ -307,8 +296,9 @@ it('should not emit color-mix() fallbacks inside @keyframes', () => {
let design = buildDesignSystem(theme)
- expect(toCss(optimizeAst(ast, design))).toMatchInlineSnapshot(`
- "@keyframes my-animation {
+ expect(pretty(toCss(optimizeAst(ast, design)))).toMatchInlineSnapshot(`
+ "
+ @keyframes my-animation {
0% {
color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent);
}
@@ -320,303 +310,1294 @@ it('should not emit color-mix() fallbacks inside @keyframes', () => {
`)
})
-it('should only visit children once when calling `replaceWith` with single element array', () => {
- let visited = new Set()
+describe('optimization', () => {
+ function optimize(input: string) {
+ let ast = CSS.parse(input)
- let ast: AstNode[] = [
- atRule('@media', '', [styleRule('.foo', [decl('color', 'blue')])]),
- styleRule('.bar', [decl('color', 'blue')]),
- ]
+ let cssOracle = pretty(toCss(handleNestingOracle(ast.map(cloneAstNode))))
+ let cssOptimized = pretty(toCss(handleNesting(ast.map(cloneAstNode))))
- walk(ast, (node) => {
- if (visited.has(node)) {
- throw new Error('Visited node twice')
- }
- visited.add(node)
+ // Ensure the results matches the slower oracle version
+ expect(cssOptimized).toEqual(cssOracle)
- if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes)
- })
-})
+ return cssOptimized
+ }
-it('should only visit children once when calling `replaceWith` with multi-element array', () => {
- let visited = new Set()
+ // See: https://drafts.csswg.org/css-nesting-1/
+ describe('CSS Nesting Module Level 1', () => {
+ it('uses the descendant combinator by default', async () => {
+ expect(
+ optimize(css`
+ .a {
+ element {
+ --x: 1;
+ }
+ .class {
+ --x: 2;
+ }
+ #id {
+ --x: 3;
+ }
+ :pseudo-class {
+ --x: 4;
+ }
+ ::pseudo-element {
+ --x: 5;
+ }
+ [attribute] {
+ --x: 6;
+ }
+ * {
+ --x: 7;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a element {
+ --x: 1;
+ }
+ .a .class {
+ --x: 2;
+ }
+ .a #id {
+ --x: 3;
+ }
+ .a :pseudo-class {
+ --x: 4;
+ }
+ .a ::pseudo-element {
+ --x: 5;
+ }
+ .a [attribute] {
+ --x: 6;
+ }
+ .a * {
+ --x: 7;
+ }
+ "
+ `)
+ })
- let ast: AstNode[] = [
- atRule('@media', '', [
- context({}, [
- styleRule('.foo', [decl('color', 'red')]),
- styleRule('.baz', [decl('color', 'blue')]),
- ]),
- ]),
- styleRule('.bar', [decl('color', 'green')]),
- ]
+ it('shoud be possible to change the combinator', async () => {
+ expect(
+ optimize(css`
+ .a {
+ + .b {
+ --x: 1;
+ }
+ > .c {
+ --x: 2;
+ }
+ ~ .d {
+ --x: 3;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a + .b {
+ --x: 1;
+ }
+ .a > .c {
+ --x: 2;
+ }
+ .a ~ .d {
+ --x: 3;
+ }
+ "
+ `)
+ })
- walk(ast, (node) => {
- let key = id(node)
- if (visited.has(key)) {
- throw new Error('Visited node twice')
- }
- visited.add(key)
+ it('should replace the first rule, that contains `&` with `:scope`', async () => {
+ expect(
+ optimize(css`
+ /* Standalone */
+ & {
+ --x: 1;
+ }
- if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes)
- })
+ /* In an at-rule */
+ @supports (--y: 1) {
+ & {
+ --x: 2;
+ }
+ }
- expect(visited).toMatchInlineSnapshot(`
- Set {
- "@media ",
- "",
- ".foo",
- "color: red",
- ".baz",
- "color: blue",
- ".bar",
- "color: green",
- }
- `)
-})
+ /* With :is(…) */
+ :is(&) {
+ --x: 3;
+ }
-it('should never visit children when calling `replaceWith` with `WalkAction.Skip`', () => {
- let visited = new Set()
+ /* In an at-rule, with :is(…) */
+ @supports (--y: 2) {
+ :is(&) {
+ --x: 4;
+ }
+ }
- let inner = styleRule('.foo', [decl('color', 'blue')])
+ /* With multiple selectors */
+ &,
+ .a {
+ --x: 5;
+ }
- let ast: AstNode[] = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])]
+ /* With multiple selectors + :is(…) */
+ :is(&),
+ .b {
+ --x: 6;
+ }
- walk(ast, (node) => {
- visited.add(node)
+ /* With multiple selectors in an at-rule */
+ @supports (--y: 3) {
+ &,
+ .c {
+ --x: 7;
+ }
+ }
- if (node.kind === 'at-rule') {
- return WalkAction.ReplaceSkip(node.nodes)
- }
- })
+ /* With multiple selectors in an at-rule + :is(…) */
+ @supports (--y: 4) {
+ :is(&),
+ .d {
+ --x: 8;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ :scope {
+ --x: 1;
+ }
+ @supports (--y: 1) {
+ :scope {
+ --x: 2;
+ }
+ }
+ :scope {
+ --x: 3;
+ }
+ @supports (--y: 2) {
+ :scope {
+ --x: 4;
+ }
+ }
+ :scope, .a {
+ --x: 5;
+ }
+ :scope, .b {
+ --x: 6;
+ }
+ @supports (--y: 3) {
+ :scope, .c {
+ --x: 7;
+ }
+ }
+ @supports (--y: 4) {
+ :scope, .d {
+ --x: 8;
+ }
+ }
+ "
+ `)
+ })
- expect(visited).not.toContain(inner)
- expect(visited).toMatchInlineSnapshot(`
- Set {
- {
- "kind": "at-rule",
- "name": "@media",
- "nodes": [
- {
- "kind": "rule",
- "nodes": [
- {
- "important": false,
- "kind": "declaration",
- "property": "color",
- "value": "blue",
- },
- ],
- "selector": ".foo",
- },
- ],
- "params": "",
- },
- {
- "kind": "rule",
- "nodes": [
- {
- "important": false,
- "kind": "declaration",
- "property": "color",
- "value": "blue",
- },
- ],
- "selector": ".bar",
- },
- {
- "important": false,
- "kind": "declaration",
- "property": "color",
- "value": "blue",
- },
- }
- `)
+ it('should be possible to use `&` to explicitly match the parent', async () => {
+ expect(
+ optimize(css`
+ .a {
+ & + .b {
+ --x: 1;
+ }
+ & > .c {
+ --x: 2;
+ }
+ & ~ .d {
+ --x: 3;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a + .b {
+ --x: 1;
+ }
+ .a > .c {
+ --x: 2;
+ }
+ .a ~ .d {
+ --x: 3;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` in a different location', async () => {
+ expect(
+ optimize(css`
+ .a {
+ .b & {
+ --x: 1;
+ }
+ .c + & {
+ --x: 2;
+ }
+ .d > & {
+ --x: 3;
+ }
+ .e ~ & {
+ --x: 4;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .b .a {
+ --x: 1;
+ }
+ .c + .a {
+ --x: 2;
+ }
+ .d > .a {
+ --x: 3;
+ }
+ .e ~ .a {
+ --x: 4;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` on its own', async () => {
+ expect(
+ optimize(css`
+ .a {
+ & {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` nested in `:is(…)`', async () => {
+ expect(
+ optimize(css`
+ .a {
+ :is(&) {
+ --x: 1;
+ }
+ :is(:is(:is(&))) {
+ --x: 2;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a {
+ --x: 1;
+ --x: 2;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to handle nesting with a parent selector list', async () => {
+ expect(
+ optimize(css`
+ .a,
+ .b {
+ .c,
+ .d & {
+ --x: 1;
+ &:hover {
+ --x: 2;
+ }
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ :is(.a, .b) .c, .d :is(.a, .b) {
+ --x: 1;
+ }
+ :is(:is(.a, .b) .c, .d :is(.a, .b)):hover {
+ --x: 2;
+ }
+ "
+ `)
+ })
+
+ it('should not replace `\&`', () => {
+ expect(
+ optimize(css`
+ .a {
+ .b-\& {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a .b-\\& {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it('should not replace `&` as part of a string', () => {
+ expect(
+ optimize(css`
+ .a {
+ [data-b='c&d'] {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a [data-b='c&d'] {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it.each([
+ ['element', '&element', 'element:is(element)'],
+ ['element', 'element&', 'element:is(element)'],
+ ['element', '&.class', 'element.class'],
+ ['element', '.class&', 'element.class'],
+ ['element', 'id', 'element#id'],
+ ['element', '#id&', 'element#id'],
+ ['element', '&:pseudo-class', 'element:pseudo-class'],
+ ['element', ':pseudo-class&', 'element:pseudo-class'],
+ ['element', '&::pseudo-element', 'element::pseudo-element'],
+ ['element', '::pseudo-element&', 'element::pseudo-element'],
+ ['element', '&:pseudo-fn()', 'element:pseudo-fn()'],
+ ['element', ':pseudo-fn()&', 'element:pseudo-fn()'],
+ ['element', '&[attribute]', 'element[attribute]'],
+ ['element', '[attribute]&', 'element[attribute]'],
+ ['element', '&*', 'element:is(*)'],
+ ['element', '*&', 'element:is(*)'],
+
+ ['.class', '&element', 'element.class'],
+ ['.class', 'element&', 'element.class'],
+ ['.class', '&.class', '.class.class'],
+ ['.class', '.class&', '.class.class'],
+ ['.class', 'id', '.class#id'],
+ ['.class', '#id&', '#id.class'],
+ ['.class', '&:pseudo-class', '.class:pseudo-class'],
+ ['.class', ':pseudo-class&', ':pseudo-class.class'],
+ ['.class', '&::pseudo-element', '.class::pseudo-element'],
+ ['.class', '::pseudo-element&', '::pseudo-element.class'],
+ ['.class', '&:pseudo-fn()', '.class:pseudo-fn()'],
+ ['.class', ':pseudo-fn()&', ':pseudo-fn().class'],
+ ['.class', '&[attribute]', '.class[attribute]'],
+ ['.class', '[attribute]&', '[attribute].class'],
+ ['.class', '&*', '*.class'],
+ ['.class', '*&', '*.class'],
+
+ ['#id', '&element', 'element#id'],
+ ['#id', 'element&', 'element#id'],
+ ['#id', '&.class', '#id.class'],
+ ['#id', '.class&', '.class#id'],
+ ['#id', 'id', '#id#id'],
+ ['#id', '#id&', '#id#id'],
+ ['#id', '&:pseudo-class', '#id:pseudo-class'],
+ ['#id', ':pseudo-class&', ':pseudo-class#id'],
+ ['#id', '&::pseudo-element', '#id::pseudo-element'],
+ ['#id', '::pseudo-element&', '::pseudo-element#id'],
+ ['#id', '&:pseudo-fn()', '#id:pseudo-fn()'],
+ ['#id', ':pseudo-fn()&', ':pseudo-fn()#id'],
+ ['#id', '&[attribute]', '#id[attribute]'],
+ ['#id', '[attribute]&', '[attribute]#id'],
+ ['#id', '&*', '*#id'],
+ ['#id', '*&', '*#id'],
+
+ [':pseudo-class', '&element', 'element:pseudo-class'],
+ [':pseudo-class', 'element&', 'element:pseudo-class'],
+ [':pseudo-class', '&.class', ':pseudo-class.class'],
+ [':pseudo-class', '.class&', '.class:pseudo-class'],
+ [':pseudo-class', 'id', ':pseudo-class#id'],
+ [':pseudo-class', '#id&', '#id:pseudo-class'],
+ [':pseudo-class', '&:pseudo-class', ':pseudo-class:pseudo-class'],
+ [':pseudo-class', ':pseudo-class&', ':pseudo-class:pseudo-class'],
+ [':pseudo-class', '&::pseudo-element', ':pseudo-class::pseudo-element'],
+ [':pseudo-class', '::pseudo-element&', '::pseudo-element:pseudo-class'],
+ [':pseudo-class', '&:pseudo-fn()', ':pseudo-class:pseudo-fn()'],
+ [':pseudo-class', ':pseudo-fn()&', ':pseudo-fn():pseudo-class'],
+ [':pseudo-class', '&[attribute]', ':pseudo-class[attribute]'],
+ [':pseudo-class', '[attribute]&', '[attribute]:pseudo-class'],
+ [':pseudo-class', '&*', '*:pseudo-class'],
+ [':pseudo-class', '*&', '*:pseudo-class'],
+
+ ['::pseudo-element', '&element', 'element::pseudo-element'],
+ ['::pseudo-element', 'element&', 'element::pseudo-element'],
+ ['::pseudo-element', '&.class', '::pseudo-element.class'],
+ ['::pseudo-element', '.class&', '.class::pseudo-element'],
+ ['::pseudo-element', 'id', '::pseudo-element#id'],
+ ['::pseudo-element', '#id&', '#id::pseudo-element'],
+ ['::pseudo-element', '&:pseudo-class', '::pseudo-element:pseudo-class'],
+ ['::pseudo-element', ':pseudo-class&', ':pseudo-class::pseudo-element'],
+ ['::pseudo-element', '&::pseudo-element', '::pseudo-element::pseudo-element'],
+ ['::pseudo-element', '::pseudo-element&', '::pseudo-element::pseudo-element'],
+ ['::pseudo-element', '&:pseudo-fn()', '::pseudo-element:pseudo-fn()'],
+ ['::pseudo-element', ':pseudo-fn()&', ':pseudo-fn()::pseudo-element'],
+ ['::pseudo-element', '&[attribute]', '::pseudo-element[attribute]'],
+ ['::pseudo-element', '[attribute]&', '[attribute]::pseudo-element'],
+ ['::pseudo-element', '&*', '*::pseudo-element'],
+ ['::pseudo-element', '*&', '*::pseudo-element'],
+
+ [':pseudo-fn()', '&element', 'element:pseudo-fn()'],
+ [':pseudo-fn()', 'element&', 'element:pseudo-fn()'],
+ [':pseudo-fn()', '&.class', ':pseudo-fn().class'],
+ [':pseudo-fn()', '.class&', '.class:pseudo-fn()'],
+ [':pseudo-fn()', 'id', ':pseudo-fn()#id'],
+ [':pseudo-fn()', '#id&', '#id:pseudo-fn()'],
+ [':pseudo-fn()', '&:pseudo-class', ':pseudo-fn():pseudo-class'],
+ [':pseudo-fn()', ':pseudo-class&', ':pseudo-class:pseudo-fn()'],
+ [':pseudo-fn()', '&::pseudo-element', ':pseudo-fn()::pseudo-element'],
+ [':pseudo-fn()', '::pseudo-element&', '::pseudo-element:pseudo-fn()'],
+ [':pseudo-fn()', '&:pseudo-fn()', ':pseudo-fn():pseudo-fn()'],
+ [':pseudo-fn()', ':pseudo-fn()&', ':pseudo-fn():pseudo-fn()'],
+ [':pseudo-fn()', '&[attribute]', ':pseudo-fn()[attribute]'],
+ [':pseudo-fn()', '[attribute]&', '[attribute]:pseudo-fn()'],
+ [':pseudo-fn()', '&*', '*:pseudo-fn()'],
+ [':pseudo-fn()', '*&', '*:pseudo-fn()'],
+
+ ['[attribute]', '&element', 'element[attribute]'],
+ ['[attribute]', 'element&', 'element[attribute]'],
+ ['[attribute]', '&.class', '[attribute].class'],
+ ['[attribute]', '.class&', '.class[attribute]'],
+ ['[attribute]', 'id', '[attribute]#id'],
+ ['[attribute]', '#id&', '#id[attribute]'],
+ ['[attribute]', '&:pseudo-class', '[attribute]:pseudo-class'],
+ ['[attribute]', ':pseudo-class&', ':pseudo-class[attribute]'],
+ ['[attribute]', '&::pseudo-element', '[attribute]::pseudo-element'],
+ ['[attribute]', '::pseudo-element&', '::pseudo-element[attribute]'],
+ ['[attribute]', '&:pseudo-fn()', '[attribute]:pseudo-fn()'],
+ ['[attribute]', ':pseudo-fn()&', ':pseudo-fn()[attribute]'],
+ ['[attribute]', '&[attribute]', '[attribute][attribute]'],
+ ['[attribute]', '[attribute]&', '[attribute][attribute]'],
+ ['[attribute]', '&*', '*[attribute]'],
+ ['[attribute]', '*&', '*[attribute]'],
+
+ ['*', '&element', 'element:is(*)'],
+ ['*', 'element&', 'element:is(*)'],
+ ['*', '&.class', '*.class'],
+ ['*', '.class&', '*.class'],
+ ['*', 'id', '*#id'],
+ ['*', '#id&', '*#id'],
+ ['*', '&:pseudo-class', '*:pseudo-class'],
+ ['*', ':pseudo-class&', '*:pseudo-class'],
+ ['*', '&::pseudo-element', '*::pseudo-element'],
+ ['*', '::pseudo-element&', '*::pseudo-element'],
+ ['*', '&:pseudo-fn()', '*:pseudo-fn()'],
+ ['*', ':pseudo-fn()&', '*:pseudo-fn()'],
+ ['*', '&[attribute]', '*[attribute]'],
+ ['*', '[attribute]&', '*[attribute]'],
+ ['*', '&*', '*:is(*)'],
+ ['*', '*&', '*:is(*)'],
+
+ ['&', '&element', 'element:scope'],
+ ['&', 'element&', 'element:scope'],
+ ['&', '&.class', ':scope.class'],
+ ['&', '.class&', '.class:scope'],
+ ['&', 'id', ':scope#id'],
+ ['&', '#id&', '#id:scope'],
+ ['&', '&:pseudo-class', ':scope:pseudo-class'],
+ ['&', ':pseudo-class&', ':pseudo-class:scope'],
+ ['&', '&::pseudo-element', ':scope::pseudo-element'],
+ ['&', '::pseudo-element&', '::pseudo-element:scope'],
+ ['&', '&:pseudo-fn()', ':scope:pseudo-fn()'],
+ ['&', ':pseudo-fn()&', ':pseudo-fn():scope'],
+ ['&', '&[attribute]', ':scope[attribute]'],
+ ['&', '[attribute]&', '[attribute]:scope'],
+ ['&', '&*', '*:scope'],
+ ['&', '*&', '*:scope'],
+ ])(`'%s { %s }' → '%s' (%#)`, async (root, nested, expected) => {
+ let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])]))
+ let ast = CSS.parse(optimized)
+
+ let count = 0
+ walk(ast, () => void count++)
+
+ // 1 rule, 1 declaration
+ expect(count).toBe(2)
+
+ if (ast[0].kind !== 'rule') throw new Error('expected a rule')
+ expect(ast[0].selector).toEqual(expected)
+ })
+
+ test('multiple selectors in the list are relative to the parent', async () => {
+ expect(
+ optimize(css`
+ .a,
+ .b {
+ --x: 1;
+ + .c {
+ --x: 2;
+ }
+ &.d {
+ --x: 3;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a, .b {
+ --x: 1;
+ }
+ :is(.a, .b) + .c {
+ --x: 2;
+ }
+ :is(.a, .b).d {
+ --x: 3;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` multiple times', async () => {
+ expect(
+ optimize(css`
+ .a {
+ & .b & .c & .d {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a .b .a .c .a .d {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` multiple times in a row', async () => {
+ expect(
+ optimize(css`
+ .a {
+ &&& {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a.a.a {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use `&` inside a selector', async () => {
+ expect(
+ optimize(css`
+ .a {
+ :not(&) {
+ --x: 1;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ :not(.a) {
+ --x: 1;
+ }
+ "
+ `)
+ })
+
+ it('should be possible to use deeply nested CSS', async () => {
+ expect(
+ optimize(css`
+ .a,
+ .b {
+ --x: 1;
+
+ .c & {
+ --x: 2;
+
+ &:hover,
+ &:focus {
+ --x: 3;
+ .d {
+ --x: 4;
+ }
+ }
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a, .b {
+ --x: 1;
+ }
+ .c :is(.a, .b) {
+ --x: 2;
+ }
+ :is(.c :is(.a, .b)):hover, :is(.c :is(.a, .b)):focus {
+ --x: 3;
+ }
+ :is(:is(.c :is(.a, .b)):hover, :is(.c :is(.a, .b)):focus) .d {
+ --x: 4;
+ }
+ "
+ `)
+ })
+
+ it('should properly split rules to guarantee specificity', async () => {
+ expect(
+ optimize(css`
+ .a {
+ --before: 1;
+ &:hover {
+ --inside: 1;
+ }
+ --after: 1;
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .a {
+ --before: 1;
+ }
+ .a:hover {
+ --inside: 1;
+ }
+ .a {
+ --after: 1;
+ }
+ "
+ `)
+ })
+
+ it('should hoist at-rules', async () => {
+ expect(
+ optimize(css`
+ @layer utilities {
+ .a,
+ .b {
+ @media (print) {
+ --x: 1;
+ .c {
+ @media (min-width: 123px) {
+ --x: 2;
+ }
+ }
+ }
+ }
+ .d {
+ @media (print) {
+ @media (min-width: 123px) {
+ --x: 3;
+ }
+ }
+ }
+ }
+ @property --foo {
+ syntax: '*';
+ }
+ @layer utilities {
+ .e {
+ @media (print) {
+ --x: 4;
+ }
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ @layer utilities {
+ @media (print) {
+ .a, .b {
+ --x: 1;
+ }
+ @media (min-width: 123px) {
+ :is(.a, .b) .c {
+ --x: 2;
+ }
+ .d {
+ --x: 3;
+ }
+ }
+ }
+ }
+ @property --foo {
+ syntax: '*';
+ }
+ @layer utilities {
+ @media (print) {
+ .e {
+ --x: 4;
+ }
+ }
+ }
+ "
+ `)
+ })
+
+ it('should leave `@property` and `@apply` alone', async () => {
+ expect(
+ optimize(css`
+ .foo {
+ .bar {
+ @apply text-red-500 hover:text-red-600;
+ }
+ }
+
+ .baz {
+ @property --tw-content {
+ syntax: '*';
+ initial-value: '';
+ inherits: false;
+ }
+
+ @property --tw-border-spacing-x {
+ syntax: '';
+ inherits: false;
+ initial-value: 0;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "
+ .foo .bar {
+ @apply text-red-500 hover:text-red-600;
+ }
+ .baz {
+ @property --tw-content {
+ syntax: '*';
+ initial-value: '';
+ inherits: false;
+ }
+ @property --tw-border-spacing-x {
+ syntax: '';
+ inherits: false;
+ initial-value: 0;
+ }
+ }
+ "
+ `)
+ })
+ })
})
-it('should skip the correct number of children based on the replaced children nodes', () => {
+// A simple step-by-step but slow implementation of CSS nesting used as an
+// oracle to test faster and more efficient solutions against.
+export function handleNestingOracle(ast: AstNode[]): AstNode[] {
+ // Remove empty nodes
+ //
+ // Inside-out such that a node containing another node that is empty, also gets
+ // cleaned up when walking up the tree.
+ //
+ // For at-rules, we only want to get rid of at-rules like `@supports` and
+ // `@media` that we know are safe to remove when they are empty.
+ //
+ // Known at-rules that are not safe to delete: `@charset`, `@layer`,
+ // `@namespace`, `@custom-media`, `@apply`, …
+ //
+ // ```css
+ // .foo {}
+ // @media (min-width: 123px) {
+ // .bar {}
+ // }
+ // @layer base;
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // @layer base;
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
- walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
+ walk(ast, {
+ exit(node) {
+ if (!('nodes' in node)) return
+ if (node.nodes.length > 0) return
+ if (node.kind === 'at-rule' && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) return
+
return WalkAction.ReplaceSkip([])
- }
+ },
})
-
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 3",
- "--index: 4",
- ]
- `)
}
+ // A rule with an `&` selector should be converted to a `:scope` if that rule
+ // has no parent rule. Parent at-rules don't count since they don't contain
+ // selectors.
+ //
+ // ```css
+ // & {
+ // color: red;
+ // }
+ // :is(&) {
+ // color: blue;
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // :scope {
+ // color: red;
+ // }
+ // :scope {
+ // color: blue;
+ // }
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
- return WalkAction.Replace([])
- }
- })
+ if (node.kind !== 'rule') return
+
+ let ast = SelectorParser.parse(node.selector)
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 3",
- "--index: 4",
- ]
- `)
+ walk(ast, (node) => {
+ // Nested in `:is(…)`, unwrap
+ while (node.kind === 'function' && node.value === ':is' && node.nodes.length === 1) {
+ node = node.nodes[0]
+ }
+
+ // Just `&`, replace with `:scope`
+ if (node.kind === 'selector' && node.value === '&') {
+ return WalkAction.ReplaceSkip(SelectorParser.selector(':scope'))
+ }
+ })
+
+ node.selector = SelectorParser.toCss(ast)
+
+ return WalkAction.Skip
+ })
}
+ // Remove intermediate nodes with an `&` selector, or `&` nested inside
+ // `:is(…)` n-levels deep, and replace them by its `nodes`.
+ //
+ // ```css
+ // .foo {
+ // & {
+ // color: red;
+ // }
+ // :is(&) {
+ // color: green;
+ // }
+ // :is(:is(&)) {
+ // color: blue;
+ // & {
+ // color: black
+ // }
+ // }
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .foo {
+ // color: red;
+ // color: green;
+ // color: blue;
+ // color: black;
+ // }
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
- return WalkAction.ReplaceSkip([decl('--index', '2.1')])
+ if (node.kind !== 'rule') return
+
+ let ast = SelectorParser.parse(node.selector)
+
+ while (
+ ast.length === 1 &&
+ ast[0].kind === 'function' &&
+ ast[0].value === ':is' &&
+ ast[0].nodes.length === 1
+ ) {
+ ast = ast[0].nodes
+ }
+
+ // Just `&`, replace by its `nodes`
+ if (ast.length === 1 && ast[0].kind === 'selector' && ast[0].value === '&') {
+ return WalkAction.Replace(node.nodes)
}
})
+ }
+
+ // Split into groups, this allows us to reduce the problem space, and only
+ // have to think about linear nesting because there will only ever be a single
+ // rule or at-rule at each level.
+ //
+ // ```css
+ // .foo {
+ // --x: 1;
+ // .bar {
+ // --x: 2;
+ // }
+ // --x: 3;
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .foo {
+ // --x: 1;
+ // }
+ // .foo {
+ // .bar {
+ // --x: 2;
+ // }
+ // }
+ // .foo {
+ // --x: 3;
+ // }
+ // ```
+ {
+ walk(ast, {
+ exit(node) {
+ if (!('nodes' in node)) return
+
+ let last: AstNode | null = null
+ let groups: AstNode[][] = []
+ for (let child of node.nodes) {
+ if (last === null || 'nodes' in child) {
+ groups.push([child])
+ } else {
+ if ('nodes' in last) {
+ groups.push([child])
+ } else {
+ groups[groups.length - 1].push(child)
+ }
+ }
+ last = child
+ }
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 3",
- "--index: 4",
- ]
- `)
+ if (groups.length <= 1) {
+ return
+ }
+
+ node.nodes = []
+ return WalkAction.Replace(
+ groups.map((nodes) => Object.assign(cloneAstNode(node), { nodes })),
+ )
+ },
+ })
}
+ // Hoist at-rules to the top in the order they were seen in
+ //
+ // ```css
+ // .foo {
+ // @media (print) {
+ // @supports (display: grid) {
+ // color: red;
+ // }
+ // }
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // @media (print) {
+ // @supports (display: grid) {
+ // .foo {
+ // color: red;
+ // }
+ // }
+ // }
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
- walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
- return WalkAction.Replace([decl('--index', '2.1')])
+ for (let [idx, node] of ast.entries()) {
+ if (!('nodes' in node)) continue
+
+ let nodes: AstNode[] = [node]
+ let atRules: [
+ name: string,
+ params: string,
+ src: SourceLocation | undefined,
+ dst: SourceLocation | undefined,
+ ][] = []
+ walk(nodes, (node) => {
+ if (node.kind !== 'at-rule') return
+ if (node.nodes.length <= 0) return
+ if (!HOISTABLE_AT_RULES.has(node.name)) return
+
+ // Track the at-rule
+ atRules.unshift([node.name, node.params, node.src, node.dst])
+
+ // Replace the at-rule by its nodes
+ return WalkAction.Replace(node.nodes)
+ })
+
+ // No at-rules found
+ if (atRules.length <= 0) continue
+
+ // Wrap `node` in the at-rules
+ {
+ let root: AstNode | null = null
+ for (let [name, params, src, dst] of atRules) {
+ root = atRule(name, params, root ? [root] : nodes)
+ if (src || dst) Object.assign(root, { src, dst })
+ }
+ if (root) ast[idx] = root
}
+ }
+ }
+
+ // Insert explicit `&`, when one was not used
+ //
+ // ```css
+ // .foo {
+ // .a, .b:is(&) {
+ // --x: 1;
+ // }
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .foo {
+ // & .a, .b:is(&) {
+ // --x: 1;
+ // }
+ // }
+ // ```
+ {
+ walk(ast, (node, ctx) => {
+ if (node.kind !== 'rule') return
+ if (!ctx.path().some((node) => node.kind === 'rule')) return // Only inject `&` when it's not the first rule
+
+ node.selector = segment(node.selector, ',')
+ .map((selector) => {
+ selector = selector.trim()
+
+ let hasAmpersand = false
+ walk(SelectorParser.parse(selector.trim()), (node) => {
+ if (node.kind === 'selector' && node.value === '&') {
+ hasAmpersand = true
+ return WalkAction.Stop
+ }
+ })
+
+ return hasAmpersand ? selector : `& ${selector}`
+ })
+ .join(', ')
})
+ }
+
+ // Flatten selectors with `:is(…)` semantics
+ //
+ // ```css
+ // .foo, .bar {
+ // &:hover {
+ // --x: 1;
+ // }
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // :is(.foo, .bar):hover {
+ // --x: 1;
+ // }
+ // ```
+ {
+ walk(ast, {
+ exit(node, ctx) {
+ if (node.kind !== 'rule') return
+ if (ctx.parent?.kind !== 'rule') return
+
+ let parentAst = SelectorParser.parse(`:is(${ctx.parent.selector})`)
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 2.1",
- "--index: 3",
- "--index: 4",
- ]
- `)
+ // Wrap parent selector in `:is(…)`
+ let ast = SelectorParser.parse(node.selector)
+ walk(ast, (node) => {
+ if (node.kind === 'selector' && node.value === '&') {
+ return WalkAction.ReplaceSkip(parentAst.map(SelectorParser.cloneAstNode))
+ }
+ })
+ ctx.parent.selector = SelectorParser.toCss(ast)
+
+ // Override the parent's nodes with our nodes now that we merged the
+ // selectors together.
+ ctx.parent.nodes = node.nodes
+ },
+ })
}
+ // Optimize the selector by unwrapping unnecessary `:is(…)`
+ //
+ // ```css
+ // .a:is(.b) {
+ // color: red;
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .a.b {
+ // color: red;
+ // }
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
- return WalkAction.ReplaceSkip([decl('--index', '2.1'), decl('--index', '2.2')])
- }
+ if (node.kind !== 'rule') return
+ node.selector = optimizeSelector(node.selector)
})
-
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 3",
- "--index: 4",
- ]
- `)
}
+ // Merge adjacent at-rules
+ //
+ // ```css
+ // @media (print) {
+ // .a, .b {
+ // --x: 1;
+ // }
+ // }
+ //
+ // @media (print) {
+ // @media (min-width: 123px) {
+ // :is(.a, .b) .c {
+ // --x: 2;
+ // }
+ // }
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // @media (print) {
+ // .a, .b {
+ // --x: 1;
+ // }
+ //
+ // @media (min-width: 123px) {
+ // :is(.a, .b) .c {
+ // --x: 2;
+ // }
+ // }
+ // }
+ // ```
{
- let ast = [
- decl('--index', '0'),
- decl('--index', '1'),
- decl('--index', '2'),
- decl('--index', '3'),
- decl('--index', '4'),
- ]
- let visited: string[] = []
- walk(ast, (node) => {
- visited.push(id(node))
- if (node.kind === 'declaration' && node.value === '2') {
- return WalkAction.Replace([decl('--index', '2.1'), decl('--index', '2.2')])
- }
+ walk(ast, {
+ enter(node, ctx) {
+ if (node.kind !== 'at-rule') return
+
+ let next = ctx.siblings[ctx.index + 1]
+
+ if (!next) return
+ if (next.kind !== 'at-rule') return
+ if (next.name !== node.name) return
+ if (next.params !== node.params) return
+
+ // Move our nodes over
+ next.nodes = node.nodes.concat(next.nodes)
+
+ // We merge everything into the last at-rule, but from a CSS perspective
+ // this should look as-if we merged it into the first one.
+ next.src = node.src
+ next.dst = node.dst
+
+ return WalkAction.Replace([])
+ },
})
+ }
- expect(visited).toMatchInlineSnapshot(`
- [
- "--index: 0",
- "--index: 1",
- "--index: 2",
- "--index: 2.1",
- "--index: 2.2",
- "--index: 3",
- "--index: 4",
- ]
- `)
+ // Merge adjacent rules with the same selector
+ //
+ // ```css
+ // .a {
+ // --x: 1;
+ // }
+ // .a {
+ // --y: 1;
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .a {
+ // --x: 1;
+ // --y: 1;
+ // }
+ // ```
+ {
+ walk(ast, {
+ enter(node, ctx) {
+ if (node.kind !== 'rule') return
+
+ let next = ctx.siblings[ctx.index + 1]
+
+ if (!next) return
+ if (next.kind !== 'rule') return
+ if (next.selector !== node.selector) return
+
+ // Move our nodes over
+ next.nodes = node.nodes.concat(next.nodes)
+
+ // We merge everything into the last at-rule, but from a CSS perspective
+ // this should look as-if we merged it into the first one.
+ next.src = node.src
+ next.dst = node.dst
+
+ return WalkAction.Replace([])
+ },
+ })
}
-})
-function id(node: AstNode) {
- switch (node.kind) {
- case 'at-rule':
- return `${node.name} ${node.params}`
- case 'rule':
- return node.selector
- case 'context':
- return ''
- case 'at-root':
- return ''
- case 'declaration':
- return `${node.property}: ${node.value}`
- case 'comment':
- return `// ${node.value}`
- default:
- node satisfies never
- throw new Error('Unknown node kind')
+ // Remove declarations that occur later with the same property / value /
+ // important information. A potential future improvement could be getting rid
+ // of overrides, but often a fallback value is used this way.
+ //
+ // ```css
+ // .foo {
+ // --x: 1;
+ // --x: 2;
+ // --x: 1;
+ // }
+ // ```
+ //
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ //
+ // ```css
+ // .foo {
+ // --x: 2;
+ // --x: 1;
+ // }
+ // ```
+ {
+ walk(ast, (node, ctx) => {
+ if (node.kind !== 'declaration') return
+
+ for (let i = ctx.index + 1; i < ctx.siblings.length; i++) {
+ let next = ctx.siblings[i]
+ if (
+ next.kind === 'declaration' &&
+ next.property === node.property &&
+ next.value === node.value &&
+ next.important === node.important
+ ) {
+ return WalkAction.Replace([])
+ }
+ }
+ })
}
+
+ return ast
}
diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts
index ce841f3776e1..93584120cd47 100644
--- a/packages/tailwindcss/src/ast.ts
+++ b/packages/tailwindcss/src/ast.ts
@@ -1,9 +1,11 @@
import { Polyfills } from '.'
+import * as SelectorParser from '../src/selector-parser'
import { parseAtRule } from './css-parser'
import type { DesignSystem } from './design-system'
import type { Source, SourceLocation } from './source-maps/source'
import { Theme, ThemeOptions } from './theme'
import { DefaultMap } from './utils/default-map'
+import { segment } from './utils/segment'
import { extractUsedVariables } from './utils/variables'
import * as ValueParser from './value-parser'
import { walk, WalkAction, type VisitContext } from './walk'
@@ -286,9 +288,9 @@ export function optimizeAst(
// found in the theme config.
if (
polyfills & Polyfills.ColorMix &&
- node.value.includes('color-mix(') &&
!context.supportsColorMix &&
- !context.keyframes
+ !context.keyframes &&
+ node.value.includes('color-mix(')
) {
colorMixDeclarations.get(parent).add(node)
}
@@ -304,37 +306,7 @@ export function optimizeAst(
transform(child, nodes, context, depth + 1)
}
- // Keep the last decl when there are exact duplicates. Keeping the *first* one might
- // not be correct when given nested rules where a rule sits between declarations.
- let seen: Record = {}
- let toRemove = new Set()
-
- // Keep track of all nodes that produce a given declaration
- for (let child of nodes) {
- if (child.kind !== 'declaration') continue
-
- let key = `${child.property}:${child.value}:${child.important}`
- seen[key] ??= []
- seen[key].push(child)
- }
-
- // And remove all but the last of each
- for (let key in seen) {
- for (let i = 0; i < seen[key].length - 1; ++i) {
- toRemove.add(seen[key][i])
- }
- }
-
- if (toRemove.size > 0) {
- nodes = nodes.filter((node) => !toRemove.has(node))
- }
-
- if (nodes.length === 0) return
-
- // Rules with `&` as the selector should be flattened
- if (node.selector === '&') {
- parent.push(...nodes)
- } else {
+ if (nodes.length > 0) {
parent.push({ ...node, nodes })
}
}
@@ -679,7 +651,463 @@ export function optimizeAst(
}
}
- return newAst
+ return handleNesting(newAst)
+}
+
+export function handleNesting(ast: AstNode[]): AstNode[] {
+ // Track `rule` selectors as we go
+ let selectorStack: [
+ selector: string,
+ src: SourceLocation | undefined,
+ dst: SourceLocation | undefined,
+ ][] = []
+
+ // Track `at-rule` information as we go. Tracking this separately from the
+ // selector stack for rules such that we can hoist this above all the rules.
+ let atRuleStack: [
+ name: string,
+ params: string,
+ src: SourceLocation | undefined,
+ dst: SourceLocation | undefined,
+ ][] = []
+
+ // The current "nodes" we can push to
+ let nodes = null as AstNode[] | null
+
+ // Optimization: Track the declaration properties we've seen in the current
+ // nodes.
+ let seenDeclarationProperties = new Set()
+
+ // Track nodes lists where we want to dedupe declarations
+ let dedupeDeclarationsInNodes = new Set()
+
+ // The final, new AST
+ let result: AstNode[] = []
+
+ // Track whether we should skip a node in the `exit` phase
+ let skipExit = new Set()
+
+ walk(ast, {
+ enter(node) {
+ switch (node.kind) {
+ case 'rule': {
+ nodes = null // Start a new level
+
+ // First time we see a rule
+ if (selectorStack.length === 0) {
+ // A rule with a selector containing `&` should replace the `&` with
+ // `:scope` if there is no parent rule.
+ //
+ // Note: there could be false positives when the `&` is escaped or
+ // part of a string inside an attribute selector. But the
+ // SelectorParser will take care of that.
+ if (node.selector.includes('&')) {
+ let ast = SelectorParser.parse(node.selector)
+ let changed = false
+
+ walk(ast, (node) => {
+ if (node.kind === 'selector' && node.value === '&') {
+ changed = true
+ node.value = ':scope'
+ }
+ })
+
+ if (changed) {
+ selectorStack.push([SelectorParser.toCss(ast), node.src, node.dst])
+ } else {
+ selectorStack.push([node.selector, node.src, node.dst])
+ }
+ }
+
+ // No nesting markers, track as-is
+ else {
+ selectorStack.push([node.selector, node.src, node.dst])
+ }
+ }
+
+ // Nested rule, ensure `&` is present in each selector. Then track the
+ // selector.
+ else {
+ // A rule with just `&` can be replaced by its children. Let's
+ // ignore this node and keep walking its children.
+ if (node.selector === '&') {
+ skipExit.add(node)
+ return
+ }
+
+ // `&` is using `:is(…)` semantics
+ let parentSelector = `:is(${selectorStack[selectorStack.length - 1][0]})`
+ let selector = segment(node.selector, ',')
+ .map((selector) => {
+ // Slow path: we need to replace the `&` with the parent
+ // selector. A simple `replaceAll(…)` won't work because a `&`
+ // could be escaped, or could be part of an attribute selector.
+ //
+ // Much safer to parse the selector and replace the `&` that way
+ if (selector.includes('&')) {
+ let ast = SelectorParser.parse(selector)
+ let changed = false
+ walk(ast, (node) => {
+ if (node.kind === 'selector' && node.value === '&') {
+ changed = true
+ node.value = parentSelector
+ }
+ })
+
+ // It could be that `&` was not found as an actual selector,
+ // in that case we still have to prepend the parent selector.
+ return changed ? SelectorParser.toCss(ast) : `${parentSelector} ${selector}`
+ }
+
+ // Fast path: we know there isn't a `&` so we can prepend the
+ // parent selector immediately.
+ else {
+ return `${parentSelector} ${selector}`
+ }
+ })
+ .join(', ')
+ selectorStack.push([selector, node.src, node.dst])
+ }
+ break
+ }
+
+ case 'at-rule': {
+ nodes = null // Start a new level
+
+ // `@layer` is hoistable, but when it's empty then we have to make
+ // sure that we still emit it because this might influence the layer
+ // order. We can't just get grid of it.
+ if (node.nodes.length === 0 && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) {
+ emit(node)
+ skipExit.add(node)
+ return WalkAction.Skip
+ }
+
+ // Hoist at-rules
+ else if (HOISTABLE_AT_RULES.has(node.name)) {
+ atRuleStack.push([node.name, node.params, node.src, node.dst])
+ }
+
+ // If we can't hoist them, emit them immediately as-is
+ else {
+ emit(node)
+ skipExit.add(node)
+ return WalkAction.Skip
+ }
+ break
+ }
+
+ case 'declaration':
+ case 'comment': {
+ emit(node)
+ break
+ }
+
+ case 'context':
+ case 'at-root':
+ break
+
+ default:
+ node satisfies never
+ break
+ }
+ },
+ exit(node) {
+ if (skipExit.delete(node)) return
+
+ switch (node.kind) {
+ case 'rule': {
+ nodes = null
+ selectorStack.pop()
+ break
+ }
+
+ case 'at-rule': {
+ nodes = null
+ atRuleStack.pop()
+ break
+ }
+
+ case 'declaration':
+ case 'comment':
+ case 'context':
+ case 'at-root':
+ break
+
+ default:
+ node satisfies never
+ break
+ }
+ },
+ })
+
+ // Dedupe declarations that we've already seen before if they match the
+ // `property`, `value` and `important` information.
+ {
+ for (let nodes of dedupeDeclarationsInNodes) {
+ let seen = new Set()
+ for (let i = nodes.length - 1; i >= 0; --i) {
+ let node = nodes[i]
+ if (node.kind !== 'declaration') continue
+
+ let id = `${node.property}\0${node.value}\0${node.important}`
+
+ if (seen.has(id)) nodes.splice(i, 1)
+ else seen.add(id)
+ }
+ }
+ }
+
+ return result
+
+ function emit(node: AstNode) {
+ // Existing nodes are available, emit into those nodes
+ if (nodes) {
+ // Optimization: track used declarations in the current node.
+ if (node.kind === 'declaration') {
+ if (seenDeclarationProperties.has(node.property)) {
+ dedupeDeclarationsInNodes.add(nodes)
+ } else {
+ seenDeclarationProperties.add(node.property)
+ }
+ }
+
+ nodes.push(node)
+ return
+ }
+
+ // Nothing available, setup a fresh node
+ {
+ // There are no parent rules or at-rules available, which means taht we
+ // can emit the node as-is.
+ if (selectorStack.length === 0 && atRuleStack.length === 0) {
+ let target = result
+ let lastNode = target[target.length - 1]
+
+ // Optimization: when the current and last node are the same, ignore the
+ // new node entirely otherwise we will get unnecessary duplicate
+ // results.
+ //
+ // We only care about at-rules with no body because some of them (such
+ // as `@charset` or `@layer`) need to be emitted. A normal rule that's
+ // empty doesn't need to be emitted.
+ if (
+ lastNode &&
+ lastNode.kind === 'at-rule' &&
+ node.kind === 'at-rule' &&
+ lastNode.name === node.name &&
+ lastNode.params === node.params
+ ) {
+ return
+ }
+
+ result.push(node)
+ return
+ }
+
+ // Track the new "parent" nodes
+ {
+ nodes = [node]
+
+ // Clear out seen declarations from the previous work in progress nodes
+ seenDeclarationProperties.clear()
+
+ // Track new declaration
+ if (node.kind === 'declaration') {
+ seenDeclarationProperties.add(node.property)
+ }
+ }
+
+ // Track the new root node that we build up to store in the final AST
+ let root = null as AstNode | null
+
+ let target = result
+ let atRuleOffset = 0
+
+ // Optimization: merge adjacent at-rules
+ //
+ // Figure out whether we can push our new node into a previous node that
+ // was already emitted.
+ //
+ // We have to make sure that the order stays the same, so therefore we
+ // only ever have to look at the last node that was emitted.
+ {
+ let lastNode = target[target.length - 1]
+ if (lastNode && lastNode.kind === 'at-rule') {
+ for (let i = 0; i < atRuleStack.length; i++) {
+ let atRule = atRuleStack[i]
+ if (lastNode.kind !== 'at-rule') break
+ if (lastNode.name !== atRule[0]) break
+ if (lastNode.params !== atRule[1]) break
+
+ atRuleOffset++
+ target = lastNode.nodes
+ lastNode = lastNode.nodes[lastNode.nodes.length - 1]
+ }
+ }
+ }
+
+ // Build up the rule
+ if (selectorStack.length > 0) {
+ let [lastSelector, src, dst] = selectorStack[selectorStack.length - 1]
+ let selector = optimizeSelector(lastSelector)
+
+ // Optimization: merge adjacent rules with the same selector
+ //
+ // Figure out whether we can push into an existing rule.
+ //
+ // If we have some at-rules that we have to keep into account, then we
+ // definitely can't.
+ if (atRuleStack.length - atRuleOffset <= 0) {
+ let lastNode = target[target.length - 1]
+ if (lastNode && lastNode.kind === 'rule' && lastNode.selector === selector) {
+ lastNode.nodes.push(...nodes)
+
+ // Ensure that our current nodes points to the nodes of the
+ // `lastNode`, otherwise we will lose information.
+ nodes = lastNode.nodes
+
+ // We appended a group that could contain declarations already in the
+ // existing rule, so let the final dedupe pass handle it once.
+ //
+ // Note: we could loop over _all_ previous nodes to figure out if we
+ // really want to dedupe this. But this could result in a bunch of
+ // duplicate work if we have `n` nodes that we want to merge
+ // together.
+ dedupeDeclarationsInNodes.add(nodes)
+
+ // We know that we don't have to handle any more at-rules, so we can
+ // bail early since we just merged the nodes with the same selector.
+ return
+ }
+ }
+
+ // Can't push into existing node, create a new node
+ root = rule(selector, nodes)
+ if (src || dst) Object.assign(root, { src, dst })
+ }
+
+ // Wrap in at-rules, if we can push into an existing node then we can
+ // ignore `offset` amount of nodes since the `root`/`nodes` will already
+ // point to a nested node.
+ for (let i = atRuleStack.length - 1; i >= atRuleOffset; --i) {
+ let [name, params, src, dst] = atRuleStack[i]
+
+ root = atRule(name, params, root ? [root] : nodes)
+ if (src || dst) Object.assign(root, { src, dst })
+ }
+
+ // Track the root node in the AST
+ if (root) {
+ target.push(root)
+ }
+
+ // We didn't build up any new root, so we can move our node directly into
+ // the target. This can happen when we emit a node that is not a
+ // declaration or a comment.
+ else {
+ target.push(...nodes)
+ }
+ }
+ }
+}
+
+// A set of at-rules that can be hoisted to the top without any repercussions.
+// Typically at-rules that rely on the environment, not parent information and
+// contain other rules/declarations.
+export const HOISTABLE_AT_RULES = new Set([
+ '@container',
+ '@layer',
+ '@media',
+ '@page',
+ '@starting-style',
+ '@supports',
+ '@view-transition',
+])
+
+// As set of at-rules that can be dropped if they don't contain any nodes. We
+// don't have the distinction between an at-rule with no body, or an at-rule
+// with a body that is empty right now.
+export const DROPPABLE_IF_EMPTY_AT_RULES = new Set([
+ '@container',
+ '@media',
+ '@page',
+ '@starting-style',
+ '@supports',
+ '@view-transition',
+])
+
+// An `element` and a `*` can only appear once, and if they do, they have
+// to be first. If multiple exist, an `:is(…)` should be used.
+//
+// - `div*` is invalid, must be `div:is(*)`
+// - `**` is invalid, must be `*:is(*)`
+// - `.classdiv` is invalid, must be `div.class`
+//
+function mustBeFirst(node: SelectorParser.SelectorAstNode | undefined) {
+ return node?.kind === 'selector' && (node.value === '*' || /^[a-zA-Z-]+$/.test(node.value))
+}
+
+let optimizedSelectorCache = new DefaultMap((selector) => {
+ let ast = SelectorParser.parse(selector)
+
+ let changed = false
+ walk(ast, {
+ exit(node, ctx) {
+ if (node.kind === 'compound') {
+ // Swap nodes in a compound selector if one of the nodes has to be first
+ let idx = node.nodes.findIndex((child) => mustBeFirst(child))
+ if (idx >= 0) {
+ // Optimization: Already in the correct spot, nothing to do here
+ if (idx === ctx.index) return
+
+ changed = true
+ node.nodes.unshift(...node.nodes.splice(idx, 1))
+ }
+ return
+ }
+
+ // Unwrap `:is(…)`
+ if (node.kind !== 'function') return
+ if (node.value !== ':is') return
+ if (node.nodes.length !== 1) return
+
+ let current = node.nodes[0]
+ if (current.kind !== 'selector' && current.kind !== 'function') return
+
+ if (ctx.parent?.kind !== 'compound') {
+ changed = true
+ return WalkAction.Replace(current)
+ }
+
+ let existing = ctx.siblings.find((sibling) => sibling !== node && mustBeFirst(sibling))
+ if (!existing) {
+ changed = true
+ return WalkAction.Replace(current)
+ }
+
+ if (!mustBeFirst(current)) {
+ changed = true
+ return WalkAction.Replace(current)
+ }
+
+ if (
+ existing.kind === 'selector' &&
+ existing.value === '*' &&
+ current.kind === 'selector' &&
+ current.value !== '*'
+ ) {
+ changed = true
+ ctx.siblings[ctx.siblings.indexOf(existing)] = SelectorParser.fun(':is', [existing])
+ return WalkAction.Replace(current)
+ }
+ },
+ })
+
+ return changed ? SelectorParser.toCss(ast) : selector
+})
+export function optimizeSelector(selector: string): string {
+ return optimizedSelectorCache.get(selector)
}
export function toCss(ast: AstNode[], track?: boolean) {
diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts
index aaba93c4e26a..8780b9358ce6 100644
--- a/packages/tailwindcss/src/compat/config.test.ts
+++ b/packages/tailwindcss/src/compat/config.test.ts
@@ -1297,22 +1297,22 @@ test('utilities must be prefixed', async () => {
// Prefixed utilities are generated
expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options))
.toMatchInlineSnapshot(`
- "
- .tw\\:custom {
- color: red;
- }
+ "
+ .tw\\:custom {
+ color: red;
+ }
- .tw\\:underline {
- text-decoration-line: underline;
- }
+ .tw\\:underline {
+ text-decoration-line: underline;
+ }
- @media (hover: hover) {
- .tw\\:hover\\:line-through:hover {
- text-decoration-line: line-through;
+ @media (hover: hover) {
+ .tw\\:hover\\:line-through:hover {
+ text-decoration-line: line-through;
+ }
}
- }
- "
- `)
+ "
+ `)
// Non-prefixed utilities are ignored
expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('')
@@ -1459,7 +1459,7 @@ test('important: `#app`', async () => {
}
@media (hover: hover) {
- #app .hover\\:line-through:hover {
+ :is(#app .hover\\:line-through):hover {
text-decoration-line: line-through;
}
}
@@ -1534,18 +1534,18 @@ test('blocklisted candidates are not generated', async () => {
// underline will as will md:bg-white
expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options))
.toMatchInlineSnapshot(`
- "
- .underline {
- text-decoration-line: underline;
- }
+ "
+ .underline {
+ text-decoration-line: underline;
+ }
- @media (min-width: 48rem) {
- .md\\:bg-white {
- background-color: var(--color-white, #fff);
+ @media (min-width: 48rem) {
+ .md\\:bg-white {
+ background-color: var(--color-white, #fff);
+ }
}
- }
- "
- `)
+ "
+ `)
})
test('blocklisted candidates cannot be used with `@apply`', async () => {
diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts
index c1d9cb57a06d..ac853982de20 100644
--- a/packages/tailwindcss/src/compat/plugin-api.test.ts
+++ b/packages/tailwindcss/src/compat/plugin-api.test.ts
@@ -2737,7 +2737,7 @@ describe('matchVariant', () => {
).toMatchInlineSnapshot(`
"
@layer utilities {
- .foo-known\\:flex:is(known), .foo-\\[test\\]\\:flex:is(test) {
+ known.foo-known\\:flex, test.foo-\\[test\\]\\:flex {
display: flex;
}
}
@@ -2775,7 +2775,7 @@ describe('matchVariant', () => {
).toMatchInlineSnapshot(`
"
@layer utilities {
- .foo-string\\:flex:is(some string), .foo-\\[test\\]\\:flex:is(test) {
+ .foo-string\\:flex:is(some string), test.foo-\\[test\\]\\:flex {
display: flex;
}
}
@@ -3204,7 +3204,7 @@ describe('addUtilities()', () => {
).toMatchInlineSnapshot(`
"
@layer utilities {
- .j.j, .j.j, .a .b:hover .c, .a .b:hover .c, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .e .bar:not(.f):has(.g), .h ~ .i, .h ~ .i {
+ .j.j, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .h ~ .i {
color: red;
}
}
diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts
index a3c9447fca71..677b1548141f 100644
--- a/packages/tailwindcss/src/important.test.ts
+++ b/packages/tailwindcss/src/important.test.ts
@@ -21,7 +21,7 @@ test('Utilities can be wrapped in a selector', async () => {
}
@media (hover: hover) {
- #app .hover\\:line-through:hover {
+ :is(#app .hover\\:line-through):hover {
text-decoration-line: line-through;
}
}
@@ -81,7 +81,7 @@ test('Utilities can be wrapped with a selector and marked as important', async (
}
@media (hover: hover) {
- #app .hover\\:line-through:hover {
+ :is(#app .hover\\:line-through):hover {
text-decoration-line: line-through !important;
}
}
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
index 2a72336fb738..74282c0058ae 100644
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -1027,7 +1027,7 @@ describe('variant stacking', () => {
expect(await run(['[&_p]:hover:flex'])).toMatchInlineSnapshot(`
"
@media (hover: hover) {
- .\\[\\&_p\\]\\:hover\\:flex p:hover {
+ :is(.\\[\\&_p\\]\\:hover\\:flex p):hover {
display: flex;
}
}
@@ -1063,7 +1063,7 @@ describe('variant stacking', () => {
}
@media (hover: hover) {
- .before\\:hover\\:flex:before:hover {
+ :is():hover {
display: flex;
}
@@ -4449,7 +4449,7 @@ describe('@custom-variant', () => {
--has-before: 1;
}
- .custom-before\\:underline:before:hover, .custom-before\\:underline:before:focus {
+ :is():hover, :is():focus {
text-decoration-line: underline;
}
}
@@ -4938,7 +4938,7 @@ describe('@custom-variant', () => {
),
).toMatchInlineSnapshot(`
"
- .a\\:flex .a, .b\\:flex .b .a .a-inside-b, .a\\:b\\:flex .a .b .a .a-inside-b, .b\\:a\\:flex .b .a .a-inside-b .a {
+ .a\\:flex .a, :is(:is(.b\\:flex .b) .a) .a-inside-b, :is(:is(:is(.a\\:b\\:flex .a) .b) .a) .a-inside-b, :is(:is(:is(.b\\:a\\:flex .b) .a) .a-inside-b) .a {
display: flex;
}
"
diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts
index 17e11f4feec6..5f76939e2e97 100644
--- a/packages/tailwindcss/src/intellisense.test.ts
+++ b/packages/tailwindcss/src/intellisense.test.ts
@@ -213,11 +213,9 @@ test('Utilities do not show wrapping selector in intellisense', async () => {
text-decoration-line: underline;
}
",
- ".hover\\:line-through {
- &:hover {
- @media (hover: hover) {
- text-decoration-line: line-through;
- }
+ "@media (hover: hover) {
+ .hover\\:line-through:hover {
+ text-decoration-line: line-through;
}
}
",
@@ -244,11 +242,9 @@ test('Utilities, when marked as important, show as important in intellisense', a
text-decoration-line: underline !important;
}
",
- ".hover\\:line-through {
- &:hover {
- @media (hover: hover) {
- text-decoration-line: line-through !important;
- }
+ "@media (hover: hover) {
+ .hover\\:line-through:hover {
+ text-decoration-line: line-through !important;
}
}
",
diff --git a/packages/tailwindcss/src/selector-parser.ts b/packages/tailwindcss/src/selector-parser.ts
index fcc78b2e90f7..ea0eb88c8952 100644
--- a/packages/tailwindcss/src/selector-parser.ts
+++ b/packages/tailwindcss/src/selector-parser.ts
@@ -70,7 +70,7 @@ function compound(nodes: SelectorAstNode[]): SelectorCompoundNode {
}
}
-function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
+export function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
return {
kind: 'function',
value,
@@ -85,7 +85,7 @@ function list(nodes: SelectorAstNode[]): SelectorListNode {
}
}
-function selector(value: string): SelectorNode {
+export function selector(value: string): SelectorNode {
return {
kind: 'selector',
value,
@@ -99,6 +99,37 @@ function value(value: string): SelectorValueNode {
}
}
+export function cloneAstNode(node: T): T {
+ switch (node.kind) {
+ case 'combinator':
+ case 'selector':
+ case 'value':
+ return {
+ kind: node.kind,
+ value: node.value,
+ } as T
+
+ case 'complex':
+ case 'compound':
+ case 'list':
+ return {
+ kind: node.kind,
+ nodes: node.nodes.map(cloneAstNode),
+ } as T
+
+ case 'function':
+ return {
+ kind: node.kind,
+ value: node.value,
+ nodes: node.nodes.map(cloneAstNode),
+ } satisfies SelectorFunctionNode as T
+
+ default:
+ node satisfies never
+ throw new Error(`Unknown node kind: ${(node as any).kind}`)
+ }
+}
+
export function toCss(ast: SelectorAstNode[], minify = false) {
let css = ''
for (let node of ast) {
diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts
index 25399e843898..fa8dfd9528f3 100644
--- a/packages/tailwindcss/src/source-maps/source-map.test.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.test.ts
@@ -462,115 +462,118 @@ test('source maps trace back to @import location', async ({ expect }) => {
^^^^^^^^^^^^^^^^^^^ EQ @ 119:6-25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 294:4-61
| 295 }
| 296 }
- 120 @supports (color: color-mix(in lab, red, red)) { |
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 120:6-53 |
- 121 color: color-mix(in oklab, currentcolor 50%, transparent); |
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 121:8-65 |
- 122 } |
- 123 } |
- 124 } |
- 125 textarea { | 302 textarea {
- ^^^^^^^^^ ER @ 125:2-11 | ^^^^^^^^^ ER @ 302:0-9
- 126 resize: vertical; | 303 resize: vertical;
- ^^^^^^^^^^^^^^^^ ES @ 126:4-20 | ^^^^^^^^^^^^^^^^ ES @ 303:2-18
+ 120 } |
+ 121 @supports (color: color-mix(in lab, red, red)) { |
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 121:4-51 |
+ 122 ::placeholder { |
+ ^^^^^^^^^^^^^^ EP @ 122:6-20 |
+ 123 color: color-mix(in oklab, currentcolor 50%, transparent); |
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 123:8-65 |
+ 124 } |
+ 125 } |
+ 126 } |
+ 127 textarea { | 302 textarea {
+ ^^^^^^^^^ ER @ 127:2-11 | ^^^^^^^^^ ER @ 302:0-9
+ 128 resize: vertical; | 303 resize: vertical;
+ ^^^^^^^^^^^^^^^^ ES @ 128:4-20 | ^^^^^^^^^^^^^^^^ ES @ 303:2-18
| 304 }
- 127 } |
- 128 ::-webkit-search-decoration { | 310 ::-webkit-search-decoration {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 128:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 310:0-28
- 129 -webkit-appearance: none; | 311 -webkit-appearance: none;
- ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 129:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 311:2-26
+ 129 } |
+ 130 ::-webkit-search-decoration { | 310 ::-webkit-search-decoration {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 130:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 310:0-28
+ 131 -webkit-appearance: none; | 311 -webkit-appearance: none;
+ ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 131:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 311:2-26
| 312 }
- 130 } |
- 131 ::-webkit-date-and-time-value { | 319 ::-webkit-date-and-time-value {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 131:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 319:0-30
- 132 min-height: 1lh; | 320 min-height: 1lh; /* 1 */
- ^^^^^^^^^^^^^^^ EW @ 132:4-19 | ^^^^^^^^^^^^^^^ EW @ 320:2-17
- 133 text-align: inherit; | 321 text-align: inherit; /* 2 */
- ^^^^^^^^^^^^^^^^^^^ EX @ 133:4-23 | ^^^^^^^^^^^^^^^^^^^ EX @ 321:2-21
+ 132 } |
+ 133 ::-webkit-date-and-time-value { | 319 ::-webkit-date-and-time-value {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 133:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 319:0-30
+ 134 min-height: 1lh; | 320 min-height: 1lh; /* 1 */
+ ^^^^^^^^^^^^^^^ EW @ 134:4-19 | ^^^^^^^^^^^^^^^ EW @ 320:2-17
+ 135 text-align: inherit; | 321 text-align: inherit; /* 2 */
+ ^^^^^^^^^^^^^^^^^^^ EX @ 135:4-23 | ^^^^^^^^^^^^^^^^^^^ EX @ 321:2-21
| 322 }
- 134 } |
- 135 ::-webkit-datetime-edit { | 328 ::-webkit-datetime-edit {
- ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 135:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 328:0-24
- 136 display: inline-flex; | 329 display: inline-flex;
- ^^^^^^^^^^^^^^^^^^^^ EZ @ 136:4-24 | ^^^^^^^^^^^^^^^^^^^^ EZ @ 329:2-22
+ 136 } |
+ 137 ::-webkit-datetime-edit { | 328 ::-webkit-datetime-edit {
+ ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 137:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 328:0-24
+ 138 display: inline-flex; | 329 display: inline-flex;
+ ^^^^^^^^^^^^^^^^^^^^ EZ @ 138:4-24 | ^^^^^^^^^^^^^^^^^^^^ EZ @ 329:2-22
| 330 }
- 137 } |
- 138 ::-webkit-datetime-edit-fields-wrapper { | 336 ::-webkit-datetime-edit-fields-wrapper {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 138:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 336:0-39
- 139 padding: 0; | 337 padding: 0;
- ^^^^^^^^^^ FB @ 139:4-14 | ^^^^^^^^^^ FB @ 337:2-12
+ 139 } |
+ 140 ::-webkit-datetime-edit-fields-wrapper { | 336 ::-webkit-datetime-edit-fields-wrapper {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 140:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 336:0-39
+ 141 padding: 0; | 337 padding: 0;
+ ^^^^^^^^^^ FB @ 141:4-14 | ^^^^^^^^^^ FB @ 337:2-12
| 338 }
- 140 } |
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 340 ::-webkit-datetime-edit,
- ^ FC @ 141:2 | ^ FC @ 340:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 341 ::-webkit-datetime-edit-year-field,
- ^ FD @ 141:2 | ^ FD @ 341:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 342 ::-webkit-datetime-edit-month-field,
- ^ FE @ 141:2 | ^ FE @ 342:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 343 ::-webkit-datetime-edit-day-field,
- ^ FF @ 141:2 | ^ FF @ 343:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 344 ::-webkit-datetime-edit-hour-field,
- ^ FG @ 141:2 | ^ FG @ 344:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit-minute-field,
- ^ FH @ 141:2 | ^ FH @ 345:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-second-field,
- ^ FI @ 141:2 | ^ FI @ 346:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-millisecond-field,
- ^ FJ @ 141:2 | ^ FJ @ 347:0
- 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-meridiem-field {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FK @ 141:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FK @ 348:0-39
- 142 padding-block: 0; | 349 padding-block: 0;
- ^^^^^^^^^^^^^^^^ FL @ 142:4-20 | ^^^^^^^^^^^^^^^^ FL @ 349:2-18
+ 142 } |
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 340 ::-webkit-datetime-edit,
+ ^ FC @ 143:2 | ^ FC @ 340:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 341 ::-webkit-datetime-edit-year-field,
+ ^ FD @ 143:2 | ^ FD @ 341:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 342 ::-webkit-datetime-edit-month-field,
+ ^ FE @ 143:2 | ^ FE @ 342:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 343 ::-webkit-datetime-edit-day-field,
+ ^ FF @ 143:2 | ^ FF @ 343:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 344 ::-webkit-datetime-edit-hour-field,
+ ^ FG @ 143:2 | ^ FG @ 344:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit-minute-field,
+ ^ FH @ 143:2 | ^ FH @ 345:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-second-field,
+ ^ FI @ 143:2 | ^ FI @ 346:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-millisecond-field,
+ ^ FJ @ 143:2 | ^ FJ @ 347:0
+ 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-meridiem-field {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FK @ 143:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FK @ 348:0-39
+ 144 padding-block: 0; | 349 padding-block: 0;
+ ^^^^^^^^^^^^^^^^ FL @ 144:4-20 | ^^^^^^^^^^^^^^^^ FL @ 349:2-18
| 350 }
- 143 } |
- 144 ::-webkit-calendar-picker-indicator { | 356 ::-webkit-calendar-picker-indicator {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 144:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 356:0-36
- 145 line-height: 1; | 357 line-height: 1;
- ^^^^^^^^^^^^^^ FN @ 145:4-18 | ^^^^^^^^^^^^^^ FN @ 357:2-16
+ 145 } |
+ 146 ::-webkit-calendar-picker-indicator { | 356 ::-webkit-calendar-picker-indicator {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 146:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 356:0-36
+ 147 line-height: 1; | 357 line-height: 1;
+ ^^^^^^^^^^^^^^ FN @ 147:4-18 | ^^^^^^^^^^^^^^ FN @ 357:2-16
| 358 }
- 146 } |
- 147 :-moz-ui-invalid { | 364 :-moz-ui-invalid {
- ^^^^^^^^^^^^^^^^^ FO @ 147:2-19 | ^^^^^^^^^^^^^^^^^ FO @ 364:0-17
- 148 box-shadow: none; | 365 box-shadow: none;
- ^^^^^^^^^^^^^^^^ FP @ 148:4-20 | ^^^^^^^^^^^^^^^^ FP @ 365:2-18
+ 148 } |
+ 149 :-moz-ui-invalid { | 364 :-moz-ui-invalid {
+ ^^^^^^^^^^^^^^^^^ FO @ 149:2-19 | ^^^^^^^^^^^^^^^^^ FO @ 364:0-17
+ 150 box-shadow: none; | 365 box-shadow: none;
+ ^^^^^^^^^^^^^^^^ FP @ 150:4-20 | ^^^^^^^^^^^^^^^^ FP @ 365:2-18
| 366 }
- 149 } |
- 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 372 button,
- ^ FQ @ 150:2 | ^ FQ @ 372:0
- 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 373 input:where([type='button'], [type='reset'], [type='submit']),
- ^ FR @ 150:2 | ^ FR @ 373:0
- 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 374 ::file-selector-button {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FS @ 150:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FS @ 374:0-23
- 151 appearance: button; | 375 appearance: button;
- ^^^^^^^^^^^^^^^^^^ FT @ 151:4-22 | ^^^^^^^^^^^^^^^^^^ FT @ 375:2-20
+ 151 } |
+ 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 372 button,
+ ^ FQ @ 152:2 | ^ FQ @ 372:0
+ 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 373 input:where([type='button'], [type='reset'], [type='submit']),
+ ^ FR @ 152:2 | ^ FR @ 373:0
+ 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 374 ::file-selector-button {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FS @ 152:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FS @ 374:0-23
+ 153 appearance: button; | 375 appearance: button;
+ ^^^^^^^^^^^^^^^^^^ FT @ 153:4-22 | ^^^^^^^^^^^^^^^^^^ FT @ 375:2-20
| 376 }
- 152 } |
- 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 382 ::-webkit-inner-spin-button,
- ^ FU @ 153:2 | ^ FU @ 382:0
- 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 383 ::-webkit-outer-spin-button {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 153:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 383:0-28
- 154 height: auto; | 384 height: auto;
- ^^^^^^^^^^^^ FW @ 154:4-16 | ^^^^^^^^^^^^ FW @ 384:2-14
+ 154 } |
+ 155 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 382 ::-webkit-inner-spin-button,
+ ^ FU @ 155:2 | ^ FU @ 382:0
+ 155 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 383 ::-webkit-outer-spin-button {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 155:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 383:0-28
+ 156 height: auto; | 384 height: auto;
+ ^^^^^^^^^^^^ FW @ 156:4-16 | ^^^^^^^^^^^^ FW @ 384:2-14
| 385 }
- 155 } |
- 156 [hidden]:where(:not([hidden='until-found'])) { | 391 [hidden]:where(:not([hidden='until-found'])) {
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 156:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 391:0-45
- 157 display: none !important; | 392 display: none !important;
- ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 157:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 392:2-26
+ 157 } |
+ 158 [hidden]:where(:not([hidden='until-found'])) { | 391 [hidden]:where(:not([hidden='until-found'])) {
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 158:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 391:0-45
+ 159 display: none !important; | 392 display: none !important;
+ ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 159:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 392:2-26
| 393 }
- 158 } |
- 159 } |
+ 160 } |
+ 161 } |
| --- index.css ---
- 160 @layer utilities; | 5 @import './utilities.css' layer(utilities);
- ^^^^^^^^^^^^^^^^ FZ @ 160:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FZ @ 5:0-42
+ 162 @layer utilities; | 5 @import './utilities.css' layer(utilities);
+ ^^^^^^^^^^^^^^^^ FZ @ 162:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FZ @ 5:0-42
| --- input.css ---
- 161 .foo { | 3 .foo {
- ^^^^^ GA @ 161:0-5 | ^^^^^ GA @ 3:0-5
- 162 text-decoration-line: underline; | 4 @apply underline;
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GB @ 162:2-33 | ^^^^^^^^^ GB @ 4:9-18
+ 163 .foo { | 3 .foo {
+ ^^^^^ GA @ 163:0-5 | ^^^^^ GA @ 3:0-5
+ 164 text-decoration-line: underline; | 4 @apply underline;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GB @ 164:2-33 | ^^^^^^^^^ GB @ 4:9-18
| 5 }
- 163 } |
- 164 |
+ 165 } |
+ 166 |
"
`)
})
@@ -695,23 +698,26 @@ test('@apply generates source maps', async ({ expect }) => {
^^^^^^^^^^^ B @ 2:2-13 | ^^^^^^^^^^^ B @ 2:2-13
3 color: #000; | 3 @apply text-[#000] hover:text-[#f00];
^^^^^^^^^^^ C @ 3:2-13 | ^^^^^^^^^^^ C @ 3:9-20
- 4 &:hover { | 3 @apply text-[#000] hover:text-[#f00];
- ^^^^^^^^ D @ 4:2-10 | ^^^^^^^^^^^^^^^^^ D @ 3:21-38
- 5 @media (hover: hover) { |
- ^^^^^^^^^^^^^^^^^^^^^^ D @ 5:4-26 |
- 6 color: #f00; |
- ^^^^^^^^^^^ D @ 6:6-17 |
- 7 } |
+ 4 } |
+ 5 @media (hover: hover) { | 3 @apply text-[#000] hover:text-[#f00];
+ ^^^^^^^^^^^^^^^^^^^^^^ D @ 5:0-22 | ^^^^^^^^^^^^^^^^^ D @ 3:21-38
+ 6 .foo:hover { |
+ ^^^^^^^^^^^ D @ 6:2-13 |
+ 7 color: #f00; |
+ ^^^^^^^^^^^ D @ 7:4-15 |
8 } |
- 9 text-decoration-line: underline; | 4 @apply underline;
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E @ 9:2-33 | ^^^^^^^^^ E @ 4:9-18
- 10 @apply --my-mixin-1 --my-mixin-2(); | 5 @apply --my-mixin-1 --my-mixin-2();
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 10:2-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 5:2-36
- 11 color: red; | 6 color: red;
- ^^^^^^^^^^ G @ 11:2-12 | ^^^^^^^^^^ G @ 6:2-12
+ 9 } |
+ 10 .foo { |
+ ^^^^^ A @ 10:0-5 |
+ 11 text-decoration-line: underline; | 4 @apply underline;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E @ 11:2-33 | ^^^^^^^^^ E @ 4:9-18
+ 12 @apply --my-mixin-1 --my-mixin-2(); | 5 @apply --my-mixin-1 --my-mixin-2();
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 12:2-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 5:2-36
+ 13 color: red; | 6 color: red;
+ ^^^^^^^^^^ G @ 13:2-12 | ^^^^^^^^^^ G @ 6:2-12
| 7 }
- 12 } |
- 13 |
+ 14 } |
+ 15 |
"
`)
})
@@ -737,49 +743,43 @@ test('@variant generates source maps', async ({ expect }) => {
expect(annotations).toMatchInlineSnapshot(`
"
- output.css | input.css
- |
- 1 .foo { | 1 .foo {
- ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5
- 2 color: red; | 2 color: red;
- ^^^^^^^^^^ B @ 2:2-12 | ^^^^^^^^^^ B @ 2:2-12
- 3 &[data-a] { |
- | 4 @variant data-a, data-b:data-c {
- 4 color: green; | 5 color: green;
- ^^^^^^^^^^^^ C @ 4:4-16 | ^^^^^^^^^^^^ C @ 5:4-16
- 5 &[data-d] { |
- | 7 @variant data-d, data-e:data-f {
- 6 color: blue; | 8 color: blue;
- ^^^^^^^^^^^ D @ 6:6-17 | ^^^^^^^^^^^ D @ 8:6-17
- | 9 }
- | 10 }
- | 11 }
- 7 } |
- 8 &[data-e] { |
- 9 &[data-f] { |
- 10 color: blue; |
- ^^^^^^^^^^^ D @ 10:8-19 |
- 11 } |
- 12 } |
- 13 } |
- 14 &[data-b] { |
- 15 &[data-c] { |
- 16 color: green; |
- ^^^^^^^^^^^^ C @ 16:6-18 |
- 17 &[data-d] { |
- 18 color: blue; |
- ^^^^^^^^^^^ D @ 18:8-19 |
- 19 } |
- 20 &[data-e] { |
- 21 &[data-f] { |
- 22 color: blue; |
- ^^^^^^^^^^^ D @ 22:10-21 |
- 23 } |
- 24 } |
- 25 } |
- 26 } |
- 27 } |
- 28 |
+ output.css | input.css
+ |
+ 1 .foo { | 1 .foo {
+ ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5
+ 2 color: red; | 2 color: red;
+ ^^^^^^^^^^ B @ 2:2-12 | ^^^^^^^^^^ B @ 2:2-12
+ 3 } |
+ 4 .foo[data-a] { |
+ | 4 @variant data-a, data-b:data-c {
+ 5 color: green; | 5 color: green;
+ ^^^^^^^^^^^^ C @ 5:2-14 | ^^^^^^^^^^^^ C @ 5:4-16
+ 6 } |
+ 7 :is(.foo[data-a])[data-... |
+ | 7 @variant data-d, data-e:data-f {
+ 8 color: blue; | 8 color: blue;
+ ^^^^^^^^^^^ D @ 8:2-13 | ^^^^^^^^^^^ D @ 8:6-17
+ | 9 }
+ | 10 }
+ | 11 }
+ 9 } |
+ 10 :is(:is(.foo[data-a])[d... |
+ 11 color: blue; |
+ ^^^^^^^^^^^ D @ 11:2-13 |
+ 12 } |
+ 13 :is(.foo[data-b])[data-... |
+ 14 color: green; |
+ ^^^^^^^^^^^^ C @ 14:2-14 |
+ 15 } |
+ 16 :is(:is(.foo[data-b])[d... |
+ 17 color: blue; |
+ ^^^^^^^^^^^ D @ 17:2-13 |
+ 18 } |
+ 19 :is(:is(:is(.foo[data-b... |
+ 20 color: blue; |
+ ^^^^^^^^^^^ D @ 20:2-13 |
+ 21 } |
+ 22 |
"
`)
})
@@ -865,25 +865,19 @@ test('Source locations for `addBase` point to the `@plugin` that generated them'
expect(annotations).toMatchInlineSnapshot(`
"
- output.css | input.css
- |
- 1 @layer base { | 1 @plugin "./plugin.js";
- ^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21
- 2 body { |
- ^^^^^ A @ 2:2-7 |
- 3 color: red; |
- ^^^^^^^^^^ A @ 3:4-14 |
- 4 } |
- 5 } |
- 6 @layer base { | 2 @config "./config.js";
- ^^^^^^^^^^^^ B @ 6:0-12 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21
- 7 body { |
- ^^^^^ B @ 7:2-7 |
- 8 color: green; |
- ^^^^^^^^^^^^ B @ 8:4-16 |
- 9 } |
- 10 } |
- 11 |
+ output.css | input.css
+ |
+ 1 @layer base { | 1 @plugin "./plugin.js";
+ ^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21
+ 2 body { |
+ ^^^^^ A @ 2:2-7 |
+ 3 color: red; |
+ ^^^^^^^^^^ A @ 3:4-14 |
+ 4 color: green; | 2 @config "./config.js";
+ ^^^^^^^^^^^^ B @ 4:4-16 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21
+ 5 } |
+ 6 } |
+ 7 |
"
`)
})
diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts
index cefcf7d03b92..d83b12827041 100644
--- a/packages/tailwindcss/src/utilities.test.ts
+++ b/packages/tailwindcss/src/utilities.test.ts
@@ -10078,57 +10078,57 @@ test('space-y-reverse', async () => {
test('divide-x', async () => {
expect(await run(['divide-x', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]']))
.toMatchInlineSnapshot(`
- "
- @layer properties {
- @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
- *, :before, :after, ::backdrop {
- --tw-divide-x-reverse: 0;
- --tw-border-style: solid;
+ "
+ @layer properties {
+ @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *, :before, :after, ::backdrop {
+ --tw-divide-x-reverse: 0;
+ --tw-border-style: solid;
+ }
}
}
- }
- :where(.divide-x > :not(:last-child)) {
- --tw-divide-x-reverse: 0;
- border-inline-style: var(--tw-border-style);
- border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));
- border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
- }
+ :where(.divide-x > :not(:last-child)) {
+ --tw-divide-x-reverse: 0;
+ border-inline-style: var(--tw-border-style);
+ border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));
+ border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
+ }
- :where(.divide-x-4 > :not(:last-child)) {
- --tw-divide-x-reverse: 0;
- border-inline-style: var(--tw-border-style);
- border-inline-start-width: calc(4px * var(--tw-divide-x-reverse));
- border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse)));
- }
+ :where(.divide-x-4 > :not(:last-child)) {
+ --tw-divide-x-reverse: 0;
+ border-inline-style: var(--tw-border-style);
+ border-inline-start-width: calc(4px * var(--tw-divide-x-reverse));
+ border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse)));
+ }
- :where(.divide-x-123 > :not(:last-child)) {
- --tw-divide-x-reverse: 0;
- border-inline-style: var(--tw-border-style);
- border-inline-start-width: calc(123px * var(--tw-divide-x-reverse));
- border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse)));
- }
+ :where(.divide-x-123 > :not(:last-child)) {
+ --tw-divide-x-reverse: 0;
+ border-inline-style: var(--tw-border-style);
+ border-inline-start-width: calc(123px * var(--tw-divide-x-reverse));
+ border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse)));
+ }
- :where(.divide-x-\\[4px\\] > :not(:last-child)) {
- --tw-divide-x-reverse: 0;
- border-inline-style: var(--tw-border-style);
- border-inline-start-width: calc(4px * var(--tw-divide-x-reverse));
- border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse)));
- }
+ :where(.divide-x-\\[4px\\] > :not(:last-child)) {
+ --tw-divide-x-reverse: 0;
+ border-inline-style: var(--tw-border-style);
+ border-inline-start-width: calc(4px * var(--tw-divide-x-reverse));
+ border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse)));
+ }
- @property --tw-divide-x-reverse {
- syntax: "*";
- inherits: false;
- initial-value: 0;
- }
+ @property --tw-divide-x-reverse {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+ }
- @property --tw-border-style {
- syntax: "*";
- inherits: false;
- initial-value: solid;
- }
- "
- `)
+ @property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+ }
+ "
+ `)
expect(
await run([
'-divide-x',
@@ -10192,61 +10192,61 @@ test('divide-x with custom default border width', async () => {
test('divide-y', async () => {
expect(await run(['divide-y', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]']))
.toMatchInlineSnapshot(`
- "
- @layer properties {
- @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
- *, :before, :after, ::backdrop {
- --tw-divide-y-reverse: 0;
- --tw-border-style: solid;
+ "
+ @layer properties {
+ @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *, :before, :after, ::backdrop {
+ --tw-divide-y-reverse: 0;
+ --tw-border-style: solid;
+ }
}
}
- }
- :where(.divide-y > :not(:last-child)) {
- --tw-divide-y-reverse: 0;
- border-bottom-style: var(--tw-border-style);
- border-top-style: var(--tw-border-style);
- border-top-width: calc(1px * var(--tw-divide-y-reverse));
- border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
- }
+ :where(.divide-y > :not(:last-child)) {
+ --tw-divide-y-reverse: 0;
+ border-bottom-style: var(--tw-border-style);
+ border-top-style: var(--tw-border-style);
+ border-top-width: calc(1px * var(--tw-divide-y-reverse));
+ border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
+ }
- :where(.divide-y-4 > :not(:last-child)) {
- --tw-divide-y-reverse: 0;
- border-bottom-style: var(--tw-border-style);
- border-top-style: var(--tw-border-style);
- border-top-width: calc(4px * var(--tw-divide-y-reverse));
- border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
- }
+ :where(.divide-y-4 > :not(:last-child)) {
+ --tw-divide-y-reverse: 0;
+ border-bottom-style: var(--tw-border-style);
+ border-top-style: var(--tw-border-style);
+ border-top-width: calc(4px * var(--tw-divide-y-reverse));
+ border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
+ }
- :where(.divide-y-123 > :not(:last-child)) {
- --tw-divide-y-reverse: 0;
- border-bottom-style: var(--tw-border-style);
- border-top-style: var(--tw-border-style);
- border-top-width: calc(123px * var(--tw-divide-y-reverse));
- border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse)));
- }
+ :where(.divide-y-123 > :not(:last-child)) {
+ --tw-divide-y-reverse: 0;
+ border-bottom-style: var(--tw-border-style);
+ border-top-style: var(--tw-border-style);
+ border-top-width: calc(123px * var(--tw-divide-y-reverse));
+ border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse)));
+ }
- :where(.divide-y-\\[4px\\] > :not(:last-child)) {
- --tw-divide-y-reverse: 0;
- border-bottom-style: var(--tw-border-style);
- border-top-style: var(--tw-border-style);
- border-top-width: calc(4px * var(--tw-divide-y-reverse));
- border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
- }
+ :where(.divide-y-\\[4px\\] > :not(:last-child)) {
+ --tw-divide-y-reverse: 0;
+ border-bottom-style: var(--tw-border-style);
+ border-top-style: var(--tw-border-style);
+ border-top-width: calc(4px * var(--tw-divide-y-reverse));
+ border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
+ }
- @property --tw-divide-y-reverse {
- syntax: "*";
- inherits: false;
- initial-value: 0;
- }
+ @property --tw-divide-y-reverse {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+ }
- @property --tw-border-style {
- syntax: "*";
- inherits: false;
- initial-value: solid;
- }
- "
- `)
+ @property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+ }
+ "
+ `)
expect(
await run([
'-divide-y',
@@ -11450,20 +11450,20 @@ test('scrollbar-width', async () => {
test('scrollbar-gutter', async () => {
expect(await run(['scrollbar-gutter-auto', 'scrollbar-gutter-stable', 'scrollbar-gutter-both']))
.toMatchInlineSnapshot(`
- "
- .scrollbar-gutter-auto {
- scrollbar-gutter: auto;
- }
+ "
+ .scrollbar-gutter-auto {
+ scrollbar-gutter: auto;
+ }
- .scrollbar-gutter-both {
- scrollbar-gutter: stable both-edges;
- }
+ .scrollbar-gutter-both {
+ scrollbar-gutter: stable both-edges;
+ }
- .scrollbar-gutter-stable {
- scrollbar-gutter: stable;
- }
- "
- `)
+ .scrollbar-gutter-stable {
+ scrollbar-gutter: stable;
+ }
+ "
+ `)
expect(
await run([
'scrollbar-gutter',
@@ -19253,104 +19253,104 @@ test('mask-y-to', async () => {
test('mask-linear', async () => {
expect(await run(['mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45']))
.toMatchInlineSnapshot(`
- "
- @layer properties {
- @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
- *, :before, :after, ::backdrop {
- --tw-mask-linear: linear-gradient(#fff, #fff);
- --tw-mask-radial: linear-gradient(#fff, #fff);
- --tw-mask-conic: linear-gradient(#fff, #fff);
- --tw-mask-linear-position: 0deg;
- --tw-mask-linear-from-position: 0%;
- --tw-mask-linear-to-position: 100%;
- --tw-mask-linear-from-color: black;
- --tw-mask-linear-to-color: transparent;
+ "
+ @layer properties {
+ @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *, :before, :after, ::backdrop {
+ --tw-mask-linear: linear-gradient(#fff, #fff);
+ --tw-mask-radial: linear-gradient(#fff, #fff);
+ --tw-mask-conic: linear-gradient(#fff, #fff);
+ --tw-mask-linear-position: 0deg;
+ --tw-mask-linear-from-position: 0%;
+ --tw-mask-linear-to-position: 100%;
+ --tw-mask-linear-from-color: black;
+ --tw-mask-linear-to-color: transparent;
+ }
}
}
- }
- .-mask-linear-45 {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
- --tw-mask-linear-position: calc(1deg * -45);
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .-mask-linear-45 {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
+ --tw-mask-linear-position: calc(1deg * -45);
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- .mask-linear-45 {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
- --tw-mask-linear-position: calc(1deg * 45);
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .mask-linear-45 {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
+ --tw-mask-linear-position: calc(1deg * 45);
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- .mask-linear-\\[3rad\\] {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
- --tw-mask-linear-position: 171.887deg;
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .mask-linear-\\[3rad\\] {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)));
+ --tw-mask-linear-position: 171.887deg;
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- @property --tw-mask-linear {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-linear {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-radial {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-radial {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-conic {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-conic {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-linear-position {
- syntax: "*";
- inherits: false;
- initial-value: 0deg;
- }
+ @property --tw-mask-linear-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0deg;
+ }
- @property --tw-mask-linear-from-position {
- syntax: "*";
- inherits: false;
- initial-value: 0%;
- }
+ @property --tw-mask-linear-from-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0%;
+ }
- @property --tw-mask-linear-to-position {
- syntax: "*";
- inherits: false;
- initial-value: 100%;
- }
+ @property --tw-mask-linear-to-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 100%;
+ }
- @property --tw-mask-linear-from-color {
- syntax: "*";
- inherits: false;
- initial-value: black;
- }
+ @property --tw-mask-linear-from-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: black;
+ }
- @property --tw-mask-linear-to-color {
- syntax: "*";
- inherits: false;
- initial-value: transparent;
- }
- "
- `)
+ @property --tw-mask-linear-to-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: transparent;
+ }
+ "
+ `)
expect(
await run([
'mask-linear',
@@ -20608,104 +20608,104 @@ test('mask-radial-to', async () => {
test('mask-conic', async () => {
expect(await run(['mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45']))
.toMatchInlineSnapshot(`
- "
- @layer properties {
- @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
- *, :before, :after, ::backdrop {
- --tw-mask-linear: linear-gradient(#fff, #fff);
- --tw-mask-radial: linear-gradient(#fff, #fff);
- --tw-mask-conic: linear-gradient(#fff, #fff);
- --tw-mask-conic-position: 0deg;
- --tw-mask-conic-from-position: 0%;
- --tw-mask-conic-to-position: 100%;
- --tw-mask-conic-from-color: black;
- --tw-mask-conic-to-color: transparent;
+ "
+ @layer properties {
+ @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *, :before, :after, ::backdrop {
+ --tw-mask-linear: linear-gradient(#fff, #fff);
+ --tw-mask-radial: linear-gradient(#fff, #fff);
+ --tw-mask-conic: linear-gradient(#fff, #fff);
+ --tw-mask-conic-position: 0deg;
+ --tw-mask-conic-from-position: 0%;
+ --tw-mask-conic-to-position: 100%;
+ --tw-mask-conic-from-color: black;
+ --tw-mask-conic-to-color: transparent;
+ }
}
}
- }
- .-mask-conic-45 {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
- --tw-mask-conic-position: calc(1deg * -45);
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .-mask-conic-45 {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
+ --tw-mask-conic-position: calc(1deg * -45);
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- .mask-conic-45 {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
- --tw-mask-conic-position: calc(1deg * 45);
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .mask-conic-45 {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
+ --tw-mask-conic-position: calc(1deg * 45);
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- .mask-conic-\\[3rad\\] {
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
- --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
- --tw-mask-conic-position: 171.887deg;
- -webkit-mask-composite: source-in;
- -webkit-mask-composite: source-in;
- mask-composite: intersect;
- }
+ .mask-conic-\\[3rad\\] {
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
+ --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)));
+ --tw-mask-conic-position: 171.887deg;
+ -webkit-mask-composite: source-in;
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ }
- @property --tw-mask-linear {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-linear {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-radial {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-radial {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-conic {
- syntax: "*";
- inherits: false;
- initial-value: linear-gradient(#fff, #fff);
- }
+ @property --tw-mask-conic {
+ syntax: "*";
+ inherits: false;
+ initial-value: linear-gradient(#fff, #fff);
+ }
- @property --tw-mask-conic-position {
- syntax: "*";
- inherits: false;
- initial-value: 0deg;
- }
+ @property --tw-mask-conic-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0deg;
+ }
- @property --tw-mask-conic-from-position {
- syntax: "*";
- inherits: false;
- initial-value: 0%;
- }
+ @property --tw-mask-conic-from-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0%;
+ }
- @property --tw-mask-conic-to-position {
- syntax: "*";
- inherits: false;
- initial-value: 100%;
- }
+ @property --tw-mask-conic-to-position {
+ syntax: "*";
+ inherits: false;
+ initial-value: 100%;
+ }
- @property --tw-mask-conic-from-color {
- syntax: "*";
- inherits: false;
- initial-value: black;
- }
+ @property --tw-mask-conic-from-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: black;
+ }
- @property --tw-mask-conic-to-color {
- syntax: "*";
- inherits: false;
- initial-value: transparent;
- }
- "
- `)
+ @property --tw-mask-conic-to-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: transparent;
+ }
+ "
+ `)
expect(
await run([
'mask-conic',
@@ -29941,20 +29941,20 @@ describe('custom utilities', () => {
`
expect(await run(['border--0', 'border--1', 'border--2'], input)).toMatchInlineSnapshot(`
- "
- .border--0 {
- border-color: var(--color-border-0, #e5e7eb);
- }
+ "
+ .border--0 {
+ border-color: var(--color-border-0, #e5e7eb);
+ }
- .border--1 {
- border-color: var(--color-border-1, #d1d5db);
- }
+ .border--1 {
+ border-color: var(--color-border-1, #d1d5db);
+ }
- .border--2 {
- border-color: var(--color-border-2, #9ca3af);
- }
- "
- `)
+ .border--2 {
+ border-color: var(--color-border-2, #9ca3af);
+ }
+ "
+ `)
expect(await run(['border--3'], input)).toEqual('')
})
@@ -30050,20 +30050,20 @@ describe('custom utilities', () => {
`
expect(await run(['example-1', 'example-76', 'example-971'], input)).toMatchInlineSnapshot(`
- "
- .example-1 {
- --resolved-value: 1;
- }
+ "
+ .example-1 {
+ --resolved-value: 1;
+ }
- .example-76 {
- --resolved-value: 76;
- }
+ .example-76 {
+ --resolved-value: 76;
+ }
- .example-971 {
- --resolved-value: 971;
- }
- "
- `)
+ .example-971 {
+ --resolved-value: 971;
+ }
+ "
+ `)
expect(await run(['example-foo'], input)).toEqual('')
})
@@ -30387,20 +30387,20 @@ describe('custom utilities', () => {
`
expect(await run(['example-a', 'example-76', 'example-[123]'], input)).toMatchInlineSnapshot(`
- "
- .example-76 {
- --resolved-value: 76;
- }
+ "
+ .example-76 {
+ --resolved-value: 76;
+ }
- .example-\\[123\\] {
- --resolved-value: 123;
- }
+ .example-\\[123\\] {
+ --resolved-value: 123;
+ }
- .example-a {
- --resolved-value: var(--example-a, 8);
- }
- "
- `)
+ .example-a {
+ --resolved-value: var(--example-a, 8);
+ }
+ "
+ `)
expect(await run(['example-[#0088cc]', 'example-[1px]'], input)).toEqual('')
})
diff --git a/packages/tailwindcss/src/utils/variables.ts b/packages/tailwindcss/src/utils/variables.ts
index 57ac4c3a5d27..b72987565a03 100644
--- a/packages/tailwindcss/src/utils/variables.ts
+++ b/packages/tailwindcss/src/utils/variables.ts
@@ -1,7 +1,8 @@
import * as ValueParser from '../value-parser'
import { walk, WalkAction } from '../walk'
+import { DefaultMap } from './default-map'
-export function extractUsedVariables(raw: string): string[] {
+let extractUsedVariablesCache = new DefaultMap((raw) => {
let variables: string[] = []
walk(ValueParser.parse(raw), (node) => {
if (node.kind !== 'function' || node.value !== 'var') return
@@ -15,4 +16,8 @@ export function extractUsedVariables(raw: string): string[] {
return WalkAction.Skip
})
return variables
+})
+
+export function extractUsedVariables(raw: string): string[] {
+ return extractUsedVariablesCache.get(raw)
}
diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts
index 305859d5b443..715d16f31675 100644
--- a/packages/tailwindcss/src/variants.test.ts
+++ b/packages/tailwindcss/src/variants.test.ts
@@ -1935,7 +1935,7 @@ test('in', async () => {
]),
).toMatchInlineSnapshot(`
"
- .not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex {
+ .not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(p:is(*)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(p:is(*)) .in-\\[p\\]\\:flex {
display: flex;
}
"
diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts
index 4d4637d8f55d..d62f13c8fb4d 100644
--- a/packages/tailwindcss/src/variants.ts
+++ b/packages/tailwindcss/src/variants.ts
@@ -11,6 +11,7 @@ import {
type Rule,
type StyleRule,
} from './ast'
+import * as AttributeSelectorParser from './attribute-selector-parser'
import { type Variant } from './candidate'
import { applyVariant } from './compile'
import type { DesignSystem } from './design-system'
@@ -845,11 +846,15 @@ export function createVariants(theme: Theme): Variants {
if (!variant.value || variant.modifier) return null
if (variant.value.kind === 'arbitrary') {
- ruleNode.nodes = [
- styleRule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
- ]
+ let selector = `[aria-${quoteAttributeValue(variant.value.value)}]`
+ let parsed = AttributeSelectorParser.parse(selector)
+ if (parsed === null) return null
+ ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
} else {
- ruleNode.nodes = [styleRule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
+ let selector = `[aria-${variant.value.value}="true"]`
+ let parsed = AttributeSelectorParser.parse(selector)
+ if (parsed === null) return null
+ ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
}
})
@@ -868,9 +873,11 @@ export function createVariants(theme: Theme): Variants {
variants.functional('data', (ruleNode, variant) => {
if (!variant.value || variant.modifier) return null
- ruleNode.nodes = [
- styleRule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
- ]
+ let selector = `[data-${quoteAttributeValue(variant.value.value)}]`
+ let parsed = AttributeSelectorParser.parse(selector)
+ if (parsed === null) return null
+
+ ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
})
variants.functional('nth', (ruleNode, variant) => {
@@ -1295,7 +1302,11 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem):
}
}
- nodes.push(node)
+ if (node.selector === '&') {
+ nodes.push(...node.nodes)
+ } else {
+ nodes.push(node)
+ }
}
// Update the variant at-rule node, to be the `&` rule node