Skip to content

Commit

Permalink
feat: Allow custom syntax
Browse files Browse the repository at this point in the history
fixes #37
  • Loading branch information
nzakas committed Feb 4, 2025
1 parent 0540006 commit 97f1060
Show file tree
Hide file tree
Showing 13 changed files with 452 additions and 31 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export default [
];
```

#### Tolerant Mode

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`:

```js
Expand All @@ -158,6 +160,73 @@ export default [

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.

#### Configuring Custom Syntax

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.

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:

```css
@my-at-rule "hello world!";
```

You can configure that syntax as follows:

```js
// eslint.config.js
import css from "@eslint/css";

export default [
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
customSyntax: {
atrules: {
"my-at-rule": {
prelude: "<string>",
},
},
},
},
rules: {
"css/no-empty-blocks": "error",
},
},
];
```

#### Configuring Tailwind Syntax

[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:

```js
// eslint.config.js
import css from "@eslint/css";
import { tailwindSyntax } from "@eslint/css/syntax";

export default [
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
customSyntax: tailwindSyntax,
},
rules: {
"css/no-empty-blocks": "error",
},
},
];
```

**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.

## License

Apache 2.0
Expand Down
6 changes: 5 additions & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
{
"name": "@eslint/css",
"version": "0.2.0",
"exports": "./dist/esm/index.js",
"exports": {
".": "./dist/esm/index.js",
"./syntax": "./dist/esm/syntax/index.js"
},
"publish": {
"include": [
"dist/esm/index.js",
"dist/esm/index.d.ts",
"dist/esm/syntax/index.js",
"README.md",
"jsr.json",
"LICENSE",
Expand Down
24 changes: 18 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@
"main": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"exports": {
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
".": {
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
},
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
"./syntax": {
"require": {
"types": "./dist/cjs/syntax/index.d.cts",
"default": "./dist/cjs/syntax/index.cjs"
},
"import": {
"types": "./dist/esm/syntax/index.d.ts",
"default": "./dist/esm/syntax/index.js"
}
}
},
"files": [
Expand Down
44 changes: 30 additions & 14 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
export default {
input: "src/index.js",
output: [
{
file: "dist/cjs/index.cjs",
format: "cjs",
},
{
file: "dist/esm/index.js",
format: "esm",
banner: '// @ts-self-types="./index.d.ts"',
},
],
};
export default [
{
input: "src/index.js",
output: [
{
file: "dist/cjs/index.cjs",
format: "cjs",
},
{
file: "dist/esm/index.js",
format: "esm",
banner: '// @ts-self-types="./index.d.ts"',
},
],
},
{
input: "src/syntax/index.js",
output: [
{
file: "dist/cjs/syntax/index.cjs",
format: "cjs",
},
{
file: "dist/esm/syntax/index.js",
format: "esm",
banner: '// @ts-self-types="./index.d.ts"',
},
],
},
];
28 changes: 26 additions & 2 deletions src/languages/css-language.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
// Imports
//------------------------------------------------------------------------------

import { parse, toPlainObject } from "css-tree";
import {
parse as originalParse,
lexer as originalLexer,
fork,
toPlainObject,
} from "css-tree";
import { CSSSourceCode } from "./css-source-code.js";
import { visitorKeys } from "./css-visitor-keys.js";

Expand All @@ -19,15 +24,18 @@ import { visitorKeys } from "./css-visitor-keys.js";
/** @typedef {import("css-tree").CssNodePlain} CssNodePlain */
/** @typedef {import("css-tree").StyleSheet} StyleSheet */
/** @typedef {import("css-tree").Comment} Comment */
/** @typedef {import("css-tree").Lexer} Lexer */
/** @typedef {import("css-tree").SyntaxConfig} SyntaxConfig */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").OkParseResult<CssNodePlain> & { comments: Comment[] }} OkParseResult */
/** @typedef {import("@eslint/core").OkParseResult<CssNodePlain> & { comments: Comment[], lexer: Lexer }} OkParseResult */
/** @typedef {import("@eslint/core").ParseResult<CssNodePlain>} ParseResult */
/** @typedef {import("@eslint/core").File} File */
/** @typedef {import("@eslint/core").FileError} FileError */

/**
* @typedef {Object} CSSLanguageOptions
* @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
* @property {SyntaxConfig} [customSyntax] Custom syntax to use for parsing.
*/

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -91,6 +99,17 @@ export class CSSLanguage {
"Expected a boolean value for 'tolerant' option.",
);
}

if ("customSyntax" in languageOptions) {
if (
typeof languageOptions.customSyntax !== "object" ||
languageOptions.customSyntax === null
) {
throw new TypeError(
"Expected an object value for 'customSyntax' option.",
);
}
}
}

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

const { tolerant } = languageOptions;
const { parse, lexer } = languageOptions.customSyntax
? fork(languageOptions.customSyntax)
: { parse: originalParse, lexer: originalLexer };

/*
* Check for parsing errors first. If there's a parsing error, nothing
Expand Down Expand Up @@ -150,6 +172,7 @@ export class CSSLanguage {
ok: true,
ast: root,
comments,
lexer,
};
} catch (ex) {
return {
Expand All @@ -170,6 +193,7 @@ export class CSSLanguage {
text: /** @type {string} */ (file.body),
ast: parseResult.ast,
comments: parseResult.comments,
lexer: parseResult.lexer,
});
}
}
11 changes: 10 additions & 1 deletion src/languages/css-source-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { visitorKeys } from "./css-visitor-keys.js";
/** @typedef {import("css-tree").CssNodePlain} CssNodePlain */
/** @typedef {import("css-tree").BlockPlain} BlockPlain */
/** @typedef {import("css-tree").Comment} Comment */
/** @typedef {import("css-tree").Lexer} Lexer */
/** @typedef {import("@eslint/core").SourceRange} SourceRange */
/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
/** @typedef {import("@eslint/core").SourceLocationWithOffset} SourceLocationWithOffset */
Expand Down Expand Up @@ -105,17 +106,25 @@ export class CSSSourceCode extends TextSourceCodeBase {
*/
comments;

/**
* The lexer for this instance.
* @type {Lexer}
*/
lexer;

/**
* Creates a new instance.
* @param {Object} options The options for the instance.
* @param {string} options.text The source code text.
* @param {CssNodePlain} options.ast The root AST node.
* @param {Array<Comment>} options.comments The comment nodes in the source code.
* @param {Lexer} options.lexer The lexer used to parse the source code.
*/
constructor({ text, ast, comments }) {
constructor({ text, ast, comments, lexer }) {
super({ text, ast });
this.ast = ast;
this.comments = comments;
this.lexer = lexer;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-invalid-at-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
// Imports
//-----------------------------------------------------------------------------

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

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -68,6 +67,7 @@ export default {

create(context) {
const { sourceCode } = context;
const lexer = sourceCode.lexer;

return {
Atrule(node) {
Expand Down
3 changes: 2 additions & 1 deletion src/rules/no-invalid-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
// Imports
//-----------------------------------------------------------------------------

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

//-----------------------------------------------------------------------------
Expand All @@ -32,6 +31,8 @@ export default {
},

create(context) {
const lexer = context.sourceCode.lexer;

return {
"Rule > Block > Declaration"(node) {
// don't validate custom properties
Expand Down
6 changes: 6 additions & 0 deletions src/syntax/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @fileoverview Common extended CSSTree syntax definitions.
* @author Nicholas C. Zakas
*/

export { default as tailwindSyntax } from "./tailwind-syntax.js";
35 changes: 35 additions & 0 deletions src/syntax/tailwind-syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @fileoverview CSSTree syntax for Tailwind CSS extensions.
* @author Nicholas C. Zakas
*/

/*
* NOTE: This file intentionally not (yet) distributed as part
* of the package. It's only used for testing purposes.
*/

export default {
atrules: {
apply: {
prelude: "<ident>+",
},
tailwind: {
prelude: "base | components | utilities",
},
config: {
prelude: "<string>",
},
},

/*
* CSSTree doesn't currently support custom functions properly, so leaving
* these out for now.
* https://github.com/csstree/csstree/issues/292
*/
// types: {
// "tailwind-theme-base": "spacing | colors",
// "tailwind-theme-color": "<tailwind-theme-base> [ '.' [ <ident> | <integer> ] ]+",
// "tailwind-theme-name": "<tailwind-theme-color>",
// "tailwind-theme()": "theme( <tailwind-theme-name>)",
// },
};
Loading

0 comments on commit 97f1060

Please sign in to comment.