Skip to content

Commit 397888b

Browse files
nzakasmdjermanovic
andauthored
feat: Allow custom syntax (#47)
* feat: Allow custom syntax fixes #37 * Update src/syntax/tailwind-syntax.js Co-authored-by: Milos Djermanovic <[email protected]> * Fix no-invalid-at-rules tests * Add tests for CSSLanguage * Generate types for /syntax * Include syntax/index.d.ts in JSR package --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 3da4963 commit 397888b

15 files changed

+501
-32
lines changed

README.md

+69
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export default [
134134
];
135135
```
136136

137+
#### Tolerant Mode
138+
137139
By default, the CSS parser runs in strict mode, which reports all parsing errors. If you'd like to allow recoverable parsing errors (those that the browser automatically fixes on its own), you can set the `tolerant` option to `true`:
138140

139141
```js
@@ -159,6 +161,73 @@ export default [
159161

160162
Setting `tolerant` to `true` is necessary if you are using custom syntax, such as [PostCSS](https://postcss.org/) plugins, that aren't part of the standard CSS syntax.
161163

164+
#### Configuring Custom Syntax
165+
166+
The CSS lexer comes prebuilt with a set of known syntax for CSS that is used in rules like `no-invalid-properties` to validate CSS code. While this works for most cases, there may be cases when you want to define your own extensions to CSS, and this can be done using the `customSyntax` language option.
167+
168+
The `customSyntax` option is an object that uses the [CSSTree format](https://github.com/csstree/csstree/blob/master/data/patch.json) for defining custom syntax, which allows you to specify at-rules, properties, and some types. For example, suppose you'd like to define a custom at-rule that looks like this:
169+
170+
```css
171+
@my-at-rule "hello world!";
172+
```
173+
174+
You can configure that syntax as follows:
175+
176+
```js
177+
// eslint.config.js
178+
import css from "@eslint/css";
179+
180+
export default [
181+
{
182+
files: ["**/*.css"],
183+
plugins: {
184+
css,
185+
},
186+
language: "css/css",
187+
languageOptions: {
188+
customSyntax: {
189+
atrules: {
190+
"my-at-rule": {
191+
prelude: "<string>",
192+
},
193+
},
194+
},
195+
},
196+
rules: {
197+
"css/no-empty-blocks": "error",
198+
},
199+
},
200+
];
201+
```
202+
203+
#### Configuring Tailwind Syntax
204+
205+
[Tailwind](https://tailwindcss.com) specifies some extensions to CSS that will otherwise be flagged as invalid by the rules in this plugin. You can configure most of the custom syntax for Tailwind using the builtin `tailwindSyntax` object, like this:
206+
207+
```js
208+
// eslint.config.js
209+
import css from "@eslint/css";
210+
import { tailwindSyntax } from "@eslint/css/syntax";
211+
212+
export default [
213+
{
214+
files: ["**/*.css"],
215+
plugins: {
216+
css,
217+
},
218+
language: "css/css",
219+
languageOptions: {
220+
customSyntax: tailwindSyntax,
221+
},
222+
rules: {
223+
"css/no-empty-blocks": "error",
224+
},
225+
},
226+
];
227+
```
228+
229+
**Note:** The Tailwind syntax doesn't currently provide for the `theme()` function. This is a [limitation of CSSTree](https://github.com/csstree/csstree/issues/292) that we hope will be resolved soon.
230+
162231
## License
163232

164233
Apache 2.0

jsr.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
{
22
"name": "@eslint/css",
33
"version": "0.2.0",
4-
"exports": "./dist/esm/index.js",
4+
"exports": {
5+
".": "./dist/esm/index.js",
6+
"./syntax": "./dist/esm/syntax/index.js"
7+
},
58
"publish": {
69
"include": [
710
"dist/esm/index.js",
811
"dist/esm/index.d.ts",
12+
"dist/esm/syntax/index.js",
13+
"dist/esm/syntax/index.d.ts",
914
"README.md",
1015
"jsr.json",
1116
"LICENSE",

package.json

+20-7
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@
77
"main": "dist/esm/index.js",
88
"types": "dist/esm/index.d.ts",
99
"exports": {
10-
"require": {
11-
"types": "./dist/cjs/index.d.cts",
12-
"default": "./dist/cjs/index.cjs"
10+
".": {
11+
"require": {
12+
"types": "./dist/cjs/index.d.cts",
13+
"default": "./dist/cjs/index.cjs"
14+
},
15+
"import": {
16+
"types": "./dist/esm/index.d.ts",
17+
"default": "./dist/esm/index.js"
18+
}
1319
},
14-
"import": {
15-
"types": "./dist/esm/index.d.ts",
16-
"default": "./dist/esm/index.js"
20+
"./syntax": {
21+
"require": {
22+
"types": "./dist/cjs/syntax/index.d.cts",
23+
"default": "./dist/cjs/syntax/index.cjs"
24+
},
25+
"import": {
26+
"types": "./dist/esm/syntax/index.d.ts",
27+
"default": "./dist/esm/syntax/index.js"
28+
}
1729
}
1830
},
1931
"files": [
@@ -51,7 +63,8 @@
5163
"scripts": {
5264
"build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
5365
"build:cts": "node -e \"fs.copyFileSync('dist/esm/index.d.ts', 'dist/cjs/index.d.cts')\"",
54-
"build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
66+
"build:syntax-cts": "node -e \"fs.copyFileSync('dist/esm/syntax/index.d.ts', 'dist/cjs/syntax/index.d.cts')\"",
67+
"build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts && tsc -p tsconfig.syntax.json && npm run build:syntax-cts",
5568
"build:readme": "node tools/update-readme.js",
5669
"build:update-rules-docs": "node tools/update-rules-docs.js",
5770
"build:baseline": "node tools/generate-baseline.js",

rollup.config.js

+30-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
1-
export default {
2-
input: "src/index.js",
3-
output: [
4-
{
5-
file: "dist/cjs/index.cjs",
6-
format: "cjs",
7-
},
8-
{
9-
file: "dist/esm/index.js",
10-
format: "esm",
11-
banner: '// @ts-self-types="./index.d.ts"',
12-
},
13-
],
14-
};
1+
export default [
2+
{
3+
input: "src/index.js",
4+
output: [
5+
{
6+
file: "dist/cjs/index.cjs",
7+
format: "cjs",
8+
},
9+
{
10+
file: "dist/esm/index.js",
11+
format: "esm",
12+
banner: '// @ts-self-types="./index.d.ts"',
13+
},
14+
],
15+
},
16+
{
17+
input: "src/syntax/index.js",
18+
output: [
19+
{
20+
file: "dist/cjs/syntax/index.cjs",
21+
format: "cjs",
22+
},
23+
{
24+
file: "dist/esm/syntax/index.js",
25+
format: "esm",
26+
banner: '// @ts-self-types="./index.d.ts"',
27+
},
28+
],
29+
},
30+
];

src/languages/css-language.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
// Imports
88
//------------------------------------------------------------------------------
99

10-
import { parse, toPlainObject } from "css-tree";
10+
import {
11+
parse as originalParse,
12+
lexer as originalLexer,
13+
fork,
14+
toPlainObject,
15+
} from "css-tree";
1116
import { CSSSourceCode } from "./css-source-code.js";
1217
import { visitorKeys } from "./css-visitor-keys.js";
1318

@@ -19,15 +24,18 @@ import { visitorKeys } from "./css-visitor-keys.js";
1924
/** @typedef {import("css-tree").CssNodePlain} CssNodePlain */
2025
/** @typedef {import("css-tree").StyleSheet} StyleSheet */
2126
/** @typedef {import("css-tree").Comment} Comment */
27+
/** @typedef {import("css-tree").Lexer} Lexer */
28+
/** @typedef {import("css-tree").SyntaxConfig} SyntaxConfig */
2229
/** @typedef {import("@eslint/core").Language} Language */
23-
/** @typedef {import("@eslint/core").OkParseResult<CssNodePlain> & { comments: Comment[] }} OkParseResult */
30+
/** @typedef {import("@eslint/core").OkParseResult<CssNodePlain> & { comments: Comment[], lexer: Lexer }} OkParseResult */
2431
/** @typedef {import("@eslint/core").ParseResult<CssNodePlain>} ParseResult */
2532
/** @typedef {import("@eslint/core").File} File */
2633
/** @typedef {import("@eslint/core").FileError} FileError */
2734

2835
/**
2936
* @typedef {Object} CSSLanguageOptions
3037
* @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
38+
* @property {SyntaxConfig} [customSyntax] Custom syntax to use for parsing.
3139
*/
3240

3341
//-----------------------------------------------------------------------------
@@ -91,6 +99,17 @@ export class CSSLanguage {
9199
"Expected a boolean value for 'tolerant' option.",
92100
);
93101
}
102+
103+
if ("customSyntax" in languageOptions) {
104+
if (
105+
typeof languageOptions.customSyntax !== "object" ||
106+
languageOptions.customSyntax === null
107+
) {
108+
throw new TypeError(
109+
"Expected an object value for 'customSyntax' option.",
110+
);
111+
}
112+
}
94113
}
95114

96115
/**
@@ -111,6 +130,9 @@ export class CSSLanguage {
111130
const errors = [];
112131

113132
const { tolerant } = languageOptions;
133+
const { parse, lexer } = languageOptions.customSyntax
134+
? fork(languageOptions.customSyntax)
135+
: { parse: originalParse, lexer: originalLexer };
114136

115137
/*
116138
* Check for parsing errors first. If there's a parsing error, nothing
@@ -150,6 +172,7 @@ export class CSSLanguage {
150172
ok: true,
151173
ast: root,
152174
comments,
175+
lexer,
153176
};
154177
} catch (ex) {
155178
return {
@@ -170,6 +193,7 @@ export class CSSLanguage {
170193
text: /** @type {string} */ (file.body),
171194
ast: parseResult.ast,
172195
comments: parseResult.comments,
196+
lexer: parseResult.lexer,
173197
});
174198
}
175199
}

src/languages/css-source-code.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { visitorKeys } from "./css-visitor-keys.js";
2323
/** @typedef {import("css-tree").CssNodePlain} CssNodePlain */
2424
/** @typedef {import("css-tree").BlockPlain} BlockPlain */
2525
/** @typedef {import("css-tree").Comment} Comment */
26+
/** @typedef {import("css-tree").Lexer} Lexer */
2627
/** @typedef {import("@eslint/core").SourceRange} SourceRange */
2728
/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
2829
/** @typedef {import("@eslint/core").SourceLocationWithOffset} SourceLocationWithOffset */
@@ -105,17 +106,25 @@ export class CSSSourceCode extends TextSourceCodeBase {
105106
*/
106107
comments;
107108

109+
/**
110+
* The lexer for this instance.
111+
* @type {Lexer}
112+
*/
113+
lexer;
114+
108115
/**
109116
* Creates a new instance.
110117
* @param {Object} options The options for the instance.
111118
* @param {string} options.text The source code text.
112119
* @param {CssNodePlain} options.ast The root AST node.
113120
* @param {Array<Comment>} options.comments The comment nodes in the source code.
121+
* @param {Lexer} options.lexer The lexer used to parse the source code.
114122
*/
115-
constructor({ text, ast, comments }) {
123+
constructor({ text, ast, comments, lexer }) {
116124
super({ text, ast });
117125
this.ast = ast;
118126
this.comments = comments;
127+
this.lexer = lexer;
119128
}
120129

121130
/**

src/rules/no-invalid-at-rules.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// Imports
88
//-----------------------------------------------------------------------------
99

10-
import { lexer } from "css-tree";
1110
import { isSyntaxMatchError } from "../util.js";
1211

1312
//-----------------------------------------------------------------------------
@@ -68,6 +67,7 @@ export default {
6867

6968
create(context) {
7069
const { sourceCode } = context;
70+
const lexer = sourceCode.lexer;
7171

7272
return {
7373
Atrule(node) {

src/rules/no-invalid-properties.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// Imports
88
//-----------------------------------------------------------------------------
99

10-
import { lexer } from "css-tree";
1110
import { isSyntaxMatchError } from "../util.js";
1211

1312
//-----------------------------------------------------------------------------
@@ -32,6 +31,8 @@ export default {
3231
},
3332

3433
create(context) {
34+
const lexer = context.sourceCode.lexer;
35+
3536
return {
3637
"Rule > Block > Declaration"(node) {
3738
// don't validate custom properties

src/syntax/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @fileoverview Common extended CSSTree syntax definitions.
3+
* @author Nicholas C. Zakas
4+
*/
5+
6+
export { default as tailwindSyntax } from "./tailwind-syntax.js";

src/syntax/tailwind-syntax.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @fileoverview CSSTree syntax for Tailwind CSS extensions.
3+
* @author Nicholas C. Zakas
4+
*/
5+
export default {
6+
atrules: {
7+
apply: {
8+
prelude: "<ident>+",
9+
},
10+
tailwind: {
11+
prelude: "base | components | utilities",
12+
},
13+
config: {
14+
prelude: "<string>",
15+
},
16+
},
17+
18+
/*
19+
* CSSTree doesn't currently support custom functions properly, so leaving
20+
* these out for now.
21+
* https://github.com/csstree/csstree/issues/292
22+
*/
23+
// types: {
24+
// "tailwind-theme-base": "spacing | colors",
25+
// "tailwind-theme-color": "<tailwind-theme-base> [ '.' [ <ident> | <integer> ] ]+",
26+
// "tailwind-theme-name": "<tailwind-theme-color>",
27+
// "tailwind-theme()": "theme( <tailwind-theme-name>)",
28+
// },
29+
};

0 commit comments

Comments
 (0)