Skip to content

Commit 35cde99

Browse files
Copilotnzakas
andcommitted
Implement type definitions and partial parser logic for slash notation
Co-authored-by: nzakas <[email protected]>
1 parent e00ec27 commit 35cde99

File tree

9 files changed

+331
-8
lines changed

9 files changed

+331
-8
lines changed

src/atrule/tailwind-apply.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,8 @@ export default {
3636
if (this.lookupType(1) === tokenTypes.Colon) {
3737
// This is a variant like hover:
3838
children.push(/** @type {ConsumerFunction} */ (this.TailwindUtilityClass)());
39-
} else if (this.lookupType(1) === tokenTypes.Delim &&
40-
this.lookupValue(1, '/') &&
41-
(this.lookupType(2) === tokenTypes.Ident || this.lookupType(2) === tokenTypes.Number)) {
42-
// This is opacity notation like outline-ring/50
43-
children.push(/** @type {ConsumerFunction} */ (this.TailwindUtilityClass)());
4439
} else {
45-
// Simple identifier
40+
// Simple identifier - don't use TailwindUtilityClass for these
4641
children.push(/** @type {ConsumerFunction} */ (this.Identifier)());
4742
}
4843

src/node/tailwind-class.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,31 @@ export function parse() {
6969
variant = className;
7070
className = this.Identifier();
7171
}
72+
73+
// Handle slash notation for opacity (e.g., bg-red-500/50 or hover:bg-red-500/50)
74+
if (this.tokenType === tokenTypes.Delim && this.tokenValue === '/') {
75+
this.next(); // consume the '/'
76+
77+
let opacityValue;
78+
if (this.tokenType === tokenTypes.Ident) {
79+
opacityValue = this.Identifier();
80+
// Merge the opacity into the class name
81+
className = {
82+
...className,
83+
name: className.name + '/' + opacityValue.name
84+
};
85+
} else if (this.tokenType === tokenTypes.Number) {
86+
// For numbers, get the token value and advance
87+
opacityValue = this.tokenValue;
88+
this.next(); // consume the number token
89+
// Merge the opacity into the class name
90+
className = {
91+
...className,
92+
name: className.name + '/' + opacityValue
93+
};
94+
}
95+
// If neither Ident nor Number, leave the slash unconsumed and let it fail naturally
96+
}
7297

7398
this.skipSC();
7499

src/tailwind3.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const tailwind3 = {
4343
"length-percentage": `${defaultSyntax.types["length-percentage"]} | <tw-theme-spacing>`,
4444
"color": `${defaultSyntax.types.color} | <tw-theme-color>`,
4545
"tw-apply-ident": "<ident> | <tw-utility-with-variant> | <tw-utility-with-opacity>",
46-
"tw-utility-with-variant": "[ <ident> ':' <ident> ]",
46+
"tw-utility-with-variant": "[ <ident> ':' <ident> ] | [ <ident> ':' <ident> '/' <number> ] | [ <ident> ':' <ident> '/' <ident> ]",
4747
"tw-utility-with-opacity": "[ <ident> '/' <number> ] | [ <ident> '/' <ident> ]",
4848
...themeTypes
4949
},

src/tailwind4.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const tailwind4 = {
7171
"tw-any-spacing": "<tw-spacing> | <tw-theme-spacing>",
7272
"tw-any-color": "<tw-alpha> | <tw-theme-color>",
7373
"tw-apply-ident": "<ident> | <tw-utility-with-variant> | <tw-utility-with-opacity>",
74-
"tw-utility-with-variant": "[ <ident> ':' <ident> ]",
74+
"tw-utility-with-variant": "[ <ident> ':' <ident> ] | [ <ident> ':' <ident> '/' <number> ] | [ <ident> ':' <ident> '/' <ident> ]",
7575
"tw-utility-with-opacity": "[ <ident> '/' <number> ] | [ <ident> '/' <ident> ]",
7676
...themeTypes
7777
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @fileoverview Add temporary debug to TailwindUtilityClass
3+
*/
4+
5+
// Let's temporarily add some debugging to the tailwind-class.js file
6+
// by creating a test that just tests the TailwindUtilityClass parsing in isolation
7+
8+
import assert from "node:assert";
9+
import { tailwind3 } from "../src/tailwind3.js";
10+
import { fork } from "@eslint/css-tree";
11+
12+
describe("Test TailwindUtilityClass in isolation", function () {
13+
it("should test if TailwindUtilityClass can parse correctly", () => {
14+
// The issue is that when we call TailwindUtilityClass on input with slash,
15+
// it throws an error. Let's test if we can identify why.
16+
17+
console.log("Testing TailwindUtilityClass behavior...");
18+
19+
// Simple test: Does a working case still work?
20+
const { parse, toPlainObject } = fork(tailwind3);
21+
22+
// Test that the existing working case still works
23+
const workingVariant = "a { @apply hover:bg-blue-500; }";
24+
const result1 = parse(workingVariant);
25+
const plain1 = toPlainObject(result1);
26+
27+
console.log("Working variant uses:", plain1.children[0].block.children[0].prelude.type);
28+
29+
if (plain1.children[0].block.children[0].prelude.type === "AtrulePrelude") {
30+
const child = plain1.children[0].block.children[0].prelude.children[0];
31+
console.log("Child type:", child.type);
32+
console.log("Child name:", child.name ? child.name.name : "N/A");
33+
console.log("Child variant:", child.variant ? child.variant.name : "N/A");
34+
}
35+
36+
// Now let's try to understand why the slash case fails
37+
// The reason it falls back to Raw is that an error occurred in parsing
38+
console.log("\nTesting slash case...");
39+
40+
const slashVariant = "a { @apply hover:bg-blue-500/50; }";
41+
const result2 = parse(slashVariant);
42+
const plain2 = toPlainObject(result2);
43+
44+
console.log("Slash variant uses:", plain2.children[0].block.children[0].prelude.type);
45+
46+
if (plain2.children[0].block.children[0].prelude.type === "Raw") {
47+
console.log("Raw value:", plain2.children[0].block.children[0].prelude.value);
48+
console.log("This means the custom parser failed and CSS-tree fell back to raw parsing");
49+
}
50+
});
51+
});

tests/debug-lexer.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @fileoverview Test lexer validation directly
3+
*/
4+
5+
import assert from "node:assert";
6+
import { tailwind3 } from "../src/tailwind3.js";
7+
import { fork } from "@eslint/css-tree";
8+
9+
describe("Debug lexer validation", function () {
10+
it("should test lexer validation", () => {
11+
const { lexer } = fork(tailwind3);
12+
13+
console.log("=== Testing lexer validation ===");
14+
15+
const test1 = lexer.matchType('tw-apply-ident', 'bg-blue-500');
16+
console.log("bg-blue-500:", test1.error === null ? "VALID" : "INVALID");
17+
18+
const test2 = lexer.matchType('tw-apply-ident', 'bg-blue-500/50');
19+
console.log("bg-blue-500/50:", test2.error === null ? "VALID" : "INVALID");
20+
21+
const test3 = lexer.matchType('tw-apply-ident', 'hover:bg-blue-500');
22+
console.log("hover:bg-blue-500:", test3.error === null ? "VALID" : "INVALID");
23+
24+
const test4 = lexer.matchType('tw-apply-ident', 'hover:bg-blue-500/50');
25+
console.log("hover:bg-blue-500/50:", test4.error === null ? "VALID" : "INVALID");
26+
27+
console.log("\n=== Testing @apply prelude validation ===");
28+
29+
const apply1 = lexer.matchAtrulePrelude('apply', 'bg-blue-500');
30+
console.log("@apply bg-blue-500:", apply1.error === null ? "VALID" : "INVALID");
31+
32+
const apply2 = lexer.matchAtrulePrelude('apply', 'bg-blue-500/50');
33+
console.log("@apply bg-blue-500/50:", apply2.error === null ? "VALID" : "INVALID");
34+
35+
const apply3 = lexer.matchAtrulePrelude('apply', 'hover:bg-blue-500');
36+
console.log("@apply hover:bg-blue-500:", apply3.error === null ? "VALID" : "INVALID");
37+
38+
const apply4 = lexer.matchAtrulePrelude('apply', 'hover:bg-blue-500/50');
39+
console.log("@apply hover:bg-blue-500/50:", apply4.error === null ? "VALID" : "INVALID");
40+
});
41+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @fileoverview Debug TailwindUtilityClass parsing specifically
3+
*/
4+
5+
import assert from "node:assert";
6+
import { tailwind3 } from "../src/tailwind3.js";
7+
import { fork } from "@eslint/css-tree";
8+
9+
describe("Debug TailwindUtilityClass behavior", function () {
10+
it("should debug what happens with slash", () => {
11+
const { parse, toPlainObject } = fork(tailwind3);
12+
13+
// Let's test step by step
14+
console.log("=== Test 1: Simple variant (should work) ===");
15+
const simple = "a { @apply hover:bg-blue-500; }";
16+
const result1 = parse(simple);
17+
const plain1 = toPlainObject(result1);
18+
console.log("Type:", plain1.children[0].block.children[0].prelude.type);
19+
if (plain1.children[0].block.children[0].prelude.type === "AtrulePrelude") {
20+
console.log("Success: Uses TailwindUtilityClass");
21+
}
22+
23+
console.log("\n=== Test 2: Simple class (should work) ===");
24+
const simple2 = "a { @apply bg-blue-500; }";
25+
const result2 = parse(simple2);
26+
const plain2 = toPlainObject(result2);
27+
console.log("Type:", plain2.children[0].block.children[0].prelude.type);
28+
if (plain2.children[0].block.children[0].prelude.type === "AtrulePrelude") {
29+
console.log("Success: Uses Identifier");
30+
}
31+
32+
console.log("\n=== Test 3: Class with slash (problem case) ===");
33+
const slash = "a { @apply bg-blue-500/50; }";
34+
const result3 = parse(slash);
35+
const plain3 = toPlainObject(result3);
36+
console.log("Type:", plain3.children[0].block.children[0].prelude.type);
37+
if (plain3.children[0].block.children[0].prelude.type === "Raw") {
38+
console.log("Failed: Falls back to Raw - this means our detection isn't working");
39+
}
40+
41+
console.log("\n=== Test 4: Variant with slash (problem case) ===");
42+
const variantSlash = "a { @apply hover:bg-blue-500/50; }";
43+
const result4 = parse(variantSlash);
44+
const plain4 = toPlainObject(result4);
45+
console.log("Type:", plain4.children[0].block.children[0].prelude.type);
46+
if (plain4.children[0].block.children[0].prelude.type === "Raw") {
47+
console.log("Failed: Falls back to Raw - this means our TailwindUtilityClass parser has an error");
48+
}
49+
});
50+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @fileoverview Isolate TailwindUtilityClass testing
3+
*/
4+
5+
import assert from "node:assert";
6+
import { tailwind3 } from "../src/tailwind3.js";
7+
import { fork } from "@eslint/css-tree";
8+
9+
describe("Isolate TailwindUtilityClass issue", function () {
10+
it("should isolate TailwindUtilityClass problem", () => {
11+
const { parse, toPlainObject } = fork(tailwind3);
12+
13+
// First, let's manually verify that a working case still works
14+
console.log("=== Working case: hover:bg-blue-500 ===");
15+
try {
16+
const result1 = parse("a { @apply hover:bg-blue-500; }");
17+
const plain1 = toPlainObject(result1);
18+
console.log("Result:", plain1.children[0].block.children[0].prelude.type);
19+
if (plain1.children[0].block.children[0].prelude.type === "AtrulePrelude") {
20+
console.log("SUCCESS: Uses AtrulePrelude with TailwindUtilityClass");
21+
}
22+
} catch (error) {
23+
console.log("ERROR:", error.message);
24+
}
25+
26+
// Now test if TailwindUtilityClass can handle a slash case
27+
console.log("\n=== Problem case: hover:bg-blue-500/50 ===");
28+
try {
29+
const result2 = parse("a { @apply hover:bg-blue-500/50; }");
30+
const plain2 = toPlainObject(result2);
31+
console.log("Result:", plain2.children[0].block.children[0].prelude.type);
32+
33+
if (plain2.children[0].block.children[0].prelude.type === "Raw") {
34+
console.log("PROBLEM: Falls back to Raw");
35+
console.log("Raw value:", plain2.children[0].block.children[0].prelude.value);
36+
37+
// This means either:
38+
// 1. The @apply parser isn't calling TailwindUtilityClass
39+
// 2. TailwindUtilityClass is throwing an error when parsing hover:bg-blue-500/50
40+
} else {
41+
console.log("SUCCESS: Uses AtrulePrelude");
42+
}
43+
} catch (error) {
44+
console.log("ERROR:", error.message);
45+
}
46+
47+
// Let's also test the non-variant slash case
48+
console.log("\n=== Non-variant slash case: bg-blue-500/50 ===");
49+
try {
50+
const result3 = parse("a { @apply bg-blue-500/50; }");
51+
const plain3 = toPlainObject(result3);
52+
console.log("Result:", plain3.children[0].block.children[0].prelude.type);
53+
54+
if (plain3.children[0].block.children[0].prelude.type === "Raw") {
55+
console.log("PROBLEM: Falls back to Raw");
56+
console.log("Raw value:", plain3.children[0].block.children[0].prelude.value);
57+
58+
// This means the @apply parser isn't detecting the slash case correctly
59+
} else {
60+
console.log("SUCCESS: Uses AtrulePrelude");
61+
}
62+
} catch (error) {
63+
console.log("ERROR:", error.message);
64+
}
65+
});
66+
});

tests/slash-notation-fix.test.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @fileoverview Test to validate the fix for slash notation
3+
*/
4+
5+
import assert from "node:assert";
6+
import { tailwind3 } from "../src/tailwind3.js";
7+
import { tailwind4 } from "../src/tailwind4.js";
8+
import { fork } from "@eslint/css-tree";
9+
10+
describe("Slash notation fix validation", function () {
11+
describe("Tailwind 3", function () {
12+
it("should parse @apply with slash notation", () => {
13+
const { parse, toPlainObject } = fork(tailwind3);
14+
15+
const result = parse("a { @apply outline-ring/50; }");
16+
const plain = toPlainObject(result);
17+
18+
const prelude = plain.children[0].block.children[0].prelude;
19+
20+
// Should NOT fall back to Raw parsing
21+
assert.notEqual(prelude.type, "Raw", "Should not fall back to Raw parsing");
22+
23+
// Should use proper AtrulePrelude
24+
assert.equal(prelude.type, "AtrulePrelude", "Should use AtrulePrelude");
25+
26+
// Should have the correct parsed structure
27+
assert.ok(prelude.children && prelude.children.length > 0, "Should have children");
28+
});
29+
30+
it("should parse @apply with variant and slash notation", () => {
31+
const { parse, toPlainObject } = fork(tailwind3);
32+
33+
const result = parse("a { @apply hover:bg-blue-500/50; }");
34+
const plain = toPlainObject(result);
35+
36+
const prelude = plain.children[0].block.children[0].prelude;
37+
38+
// Should NOT fall back to Raw parsing
39+
assert.notEqual(prelude.type, "Raw", "Should not fall back to Raw parsing");
40+
41+
// Should use proper AtrulePrelude
42+
assert.equal(prelude.type, "AtrulePrelude", "Should use AtrulePrelude");
43+
44+
// Should have TailwindUtilityClass with slash in name
45+
assert.ok(prelude.children && prelude.children.length > 0, "Should have children");
46+
47+
const child = prelude.children[0];
48+
if (child.type === "TailwindUtilityClass") {
49+
assert.ok(child.name.name.includes('/'), "Should include slash in class name");
50+
}
51+
});
52+
53+
it("should parse complex @apply from the original issue", () => {
54+
const { parse, toPlainObject } = fork(tailwind3);
55+
56+
const css = `
57+
@layer base {
58+
* {
59+
@apply border-border outline-ring/50;
60+
}
61+
body {
62+
@apply bg-background text-foreground;
63+
}
64+
}
65+
`;
66+
67+
const result = parse(css);
68+
const plain = toPlainObject(result);
69+
70+
// Find the @apply rule
71+
const layerRule = plain.children[0]; // @layer
72+
const starRule = layerRule.block.children[0]; // *
73+
const applyRule = starRule.block.children[0]; // @apply
74+
75+
// Should NOT fall back to Raw parsing
76+
assert.notEqual(applyRule.prelude.type, "Raw", "Should not fall back to Raw parsing");
77+
assert.equal(applyRule.prelude.type, "AtrulePrelude", "Should use AtrulePrelude");
78+
});
79+
});
80+
81+
describe("Tailwind 4", function () {
82+
it("should parse @apply with slash notation", () => {
83+
const { parse, toPlainObject } = fork(tailwind4);
84+
85+
const result = parse("a { @apply outline-ring/50; }");
86+
const plain = toPlainObject(result);
87+
88+
const prelude = plain.children[0].block.children[0].prelude;
89+
90+
// Should NOT fall back to Raw parsing
91+
assert.notEqual(prelude.type, "Raw", "Should not fall back to Raw parsing");
92+
assert.equal(prelude.type, "AtrulePrelude", "Should use AtrulePrelude");
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)