Skip to content

Commit 8d5cd4c

Browse files
authored
fix: Support @import source parsing (#30)
Revert "Revert "Support `@import` parsing" (#29)" This reverts commit cb9bad8.
1 parent cb9bad8 commit 8d5cd4c

File tree

4 files changed

+665
-56
lines changed

4 files changed

+665
-56
lines changed

src/atrule/tailwind-import.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @fileoverview Tailwind 4 `@import` rule parser
3+
* @see https://github.com/eslint/csstree/blob/1d74355c1960315bf55a33b0652d13f97ebb1ba2/lib/syntax/atrule/import.js.
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Imports
8+
//-----------------------------------------------------------------------------
9+
10+
import { tokenTypes } from "@eslint/css-tree";
11+
12+
//-----------------------------------------------------------------------------
13+
// Type Definitions
14+
//-----------------------------------------------------------------------------
15+
16+
/**
17+
* @import { ParserContext as BaseParserContext, ConsumerFunction, SyntaxConfig } from "@eslint/css-tree";
18+
*/
19+
20+
/**
21+
* @typedef {'Raw' | 'Layer' | 'Condition' | 'Declaration' | 'String' | 'Identifier' | 'Url' | 'Function' | 'MediaQueryList'} ConsumerNames
22+
* @typedef {BaseParserContext & { [key in ConsumerNames]: ConsumerFunction }} ParserContext
23+
*/
24+
25+
//-----------------------------------------------------------------------------
26+
// Helpers
27+
//-----------------------------------------------------------------------------
28+
29+
/**
30+
* @param {ConsumerFunction} parse
31+
* @param {ConsumerFunction=} fallback
32+
* @this {ParserContext}
33+
*/
34+
function parseWithFallback(parse, fallback) {
35+
return this.parseWithFallback(
36+
() => {
37+
try {
38+
return parse.call(this);
39+
} finally {
40+
this.skipSC();
41+
if (this.lookupNonWSType(0) !== tokenTypes.RightParenthesis) {
42+
// @ts-expect-error
43+
this.error();
44+
}
45+
}
46+
},
47+
fallback || (() => this.Raw(null, true))
48+
);
49+
}
50+
51+
const parseFunctions = {
52+
/**
53+
* @this {ParserContext}
54+
*/
55+
layer() {
56+
this.skipSC();
57+
58+
const children = this.createList();
59+
const node = parseWithFallback.call(this, this.Layer);
60+
61+
if (node.type !== 'Raw' || node.value !== '') {
62+
children.push(node);
63+
}
64+
65+
return children;
66+
},
67+
/**
68+
* @this {ParserContext}
69+
*/
70+
supports() {
71+
this.skipSC();
72+
73+
const children = this.createList();
74+
const node = parseWithFallback.call(
75+
this,
76+
this.Declaration,
77+
() => parseWithFallback.call(this, () => this.Condition('supports'))
78+
);
79+
80+
if (node.type !== 'Raw' || node.value !== '') {
81+
children.push(node);
82+
}
83+
84+
return children;
85+
},
86+
/**
87+
* @this {ParserContext}
88+
*/
89+
source() {
90+
this.skipSC();
91+
92+
const children = this.createList();
93+
const node = parseWithFallback.call(
94+
this,
95+
this.String,
96+
() => parseWithFallback.call(this, this.Identifier)
97+
);
98+
99+
if (node.type !== 'Raw' || node.value !== '') {
100+
children.push(node);
101+
}
102+
103+
return children;
104+
},
105+
/**
106+
* @this {ParserContext}
107+
*/
108+
prefix() {
109+
this.skipSC();
110+
111+
const children = this.createList();
112+
const node = parseWithFallback.call(this, this.Identifier);
113+
114+
if (node.type !== 'Raw' || node.value !== '') {
115+
children.push(node);
116+
}
117+
118+
return children;
119+
},
120+
};
121+
122+
//-----------------------------------------------------------------------------
123+
// Exports
124+
//-----------------------------------------------------------------------------
125+
126+
export default {
127+
parse: {
128+
129+
/**
130+
* @this {ParserContext}
131+
* @type {SyntaxConfig['atrule']['import']['parse']['prelude']}
132+
*/
133+
// @ts-expect-error it doesn't like that we extend ParserContext
134+
prelude: function() {
135+
const children = this.createList();
136+
137+
switch (this.tokenType) {
138+
case tokenTypes.String:
139+
children.push(this.String());
140+
break;
141+
142+
case tokenTypes.Url:
143+
case tokenTypes.Function:
144+
children.push(this.Url());
145+
break;
146+
147+
default:
148+
// @ts-expect-error
149+
this.error('String or url() is expected');
150+
}
151+
152+
while (true) {
153+
this.skipSC();
154+
155+
if (
156+
this.tokenType === tokenTypes.Function && (
157+
this.cmpStr(this.tokenStart, this.tokenEnd, 'source(') ||
158+
this.cmpStr(this.tokenStart, this.tokenEnd, 'prefix(') ||
159+
this.cmpStr(this.tokenStart, this.tokenEnd, 'layer(')
160+
)
161+
) {
162+
children.push(this.Function(null, parseFunctions));
163+
} else if (
164+
this.tokenType === tokenTypes.Ident &&
165+
this.cmpStr(this.tokenStart, this.tokenEnd, 'layer')
166+
) {
167+
children.push(this.Identifier());
168+
} else {
169+
break;
170+
}
171+
}
172+
173+
this.skipSC();
174+
175+
if (this.tokenType === tokenTypes.Function &&
176+
this.cmpStr(this.tokenStart, this.tokenEnd, 'supports(')) {
177+
children.push(this.Function(null, parseFunctions));
178+
}
179+
180+
if (this.lookupNonWSType(0) === tokenTypes.Ident ||
181+
this.lookupNonWSType(0) === tokenTypes.LeftParenthesis) {
182+
children.push(this.MediaQueryList());
183+
}
184+
185+
return children;
186+
},
187+
block: null
188+
}
189+
};

src/tailwind4.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import defaultSyntax from "@eslint/css-tree/definition-syntax-data";
1111
import * as TailwindThemeKey from "./node/tailwind-theme-key.js";
1212
import * as TailwindUtilityClass from "./node/tailwind-class.js";
1313
import tailwindApply from "./atrule/tailwind-apply.js";
14+
import tailwindImport from "./atrule/tailwind-import.js";
1415
import theme from "./scope/theme.js";
1516
import { themeTypes } from "./types/theme-types.js";
1617

@@ -19,16 +20,19 @@ import { themeTypes } from "./types/theme-types.js";
1920
//-----------------------------------------------------------------------------
2021

2122
/**
22-
* @typedef {import("@eslint/css-tree").NodeSyntaxConfig} NodeSyntaxConfig
2323
* @import { SyntaxConfig } from "@eslint/css-tree"
2424
*/
2525

2626
/** @type {Partial<SyntaxConfig>} */
2727
export const tailwind4 = {
2828
atrule: {
2929
apply: tailwindApply,
30+
import: tailwindImport,
3031
},
3132
atrules: {
33+
import: {
34+
prelude: "[ <string> | <url> ] [ [ source( [ <string> | none ] ) ]? || [ prefix( <ident> ) ]? || [ layer | layer( <layer-name> ) ]? ] [ supports( [ <supports-condition> | <declaration> ] ) ]? <media-query-list>?",
35+
},
3236
apply: {
3337
prelude: "<tw-apply-ident>+",
3438
},

tests/tailwind3.test.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,24 +367,6 @@ describe("Tailwind 3", function () {
367367
});
368368
});
369369

370-
371-
describe("@import", () => {
372-
it("should parse @import with prefix function without crashing", () => {
373-
// This is a regression test for the issue where prefix() functions
374-
// cause parsing to crash. While the prefix function isn't parsed
375-
// into a structured format, it should not crash and should preserve
376-
// the original content.
377-
const tree = toPlainObject(parse("@import 'tailwindcss' prefix(foo);"));
378-
379-
assert.strictEqual(tree.children[0].type, "Atrule");
380-
assert.strictEqual(tree.children[0].name, "import");
381-
382-
// The prefix function content should be preserved
383-
assert.ok(tree.children[0].prelude.value.includes("'tailwindcss'"));
384-
assert.ok(tree.children[0].prelude.value.includes("prefix(foo)"));
385-
});
386-
});
387-
388370
describe("Canonical Tailwind 3 File", () => {
389371
it("should parse a canonical Tailwind 3 file", async () => {
390372
const file = await fs.readFile(filename, "utf8");

0 commit comments

Comments
 (0)