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 Jan 6, 2025
1 parent 37984bd commit 4e0c041
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 19 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@ export default [
<!-- NOTE: The following table is autogenerated. Do not manually edit. -->

<!-- Rule Table Start -->

| **Rule Name** | **Description** | **Recommended** |
| :--------------------------------------------------------------- | :------------------------------- | :-------------: |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |

| **Rule Name** | **Description** | **Recommended** |
| :- | :- | :-: |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
<!-- Rule Table End -->

**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.
Expand Down
18 changes: 15 additions & 3 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,8 +24,9 @@ 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("@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 */
Expand Down Expand Up @@ -78,7 +84,7 @@ export class CSSLanguage {
* @param {File} file The virtual file to parse.
* @returns {ParseResult} The result of parsing.
*/
parse(file) {
parse(file, { languageOptions }) {
// Note: BOM already removed
const text = /** @type {string} */ (file.body);

Expand All @@ -88,6 +94,10 @@ export class CSSLanguage {
/** @type {FileError[]} */
const errors = [];

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

/*
* Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error
Expand Down Expand Up @@ -124,6 +134,7 @@ export class CSSLanguage {
ok: true,
ast: root,
comments,
lexer,
};
} catch (ex) {
return {
Expand All @@ -144,6 +155,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
24 changes: 24 additions & 0 deletions src/syntax/tailwind-syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @fileoverview CSSTree syntax for Tailwind CSS extensions.
* @author Nicholas C. Zakas
*/

export default {
atrules: {
apply: {
prelude: "<ident>+",
},
tailwind: {
prelude: "base | components | utilities",
},
config: {
prelude: "<string>",
},
},
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>)",
},
};
99 changes: 99 additions & 0 deletions tests/rules/no-invalid-at-rules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import rule from "../../src/rules/no-invalid-at-rules.js";
import css from "../../src/index.js";
import tailwindSyntax from "../../src/syntax/tailwind-syntax.js";
import { RuleTester } from "eslint";

//------------------------------------------------------------------------------
Expand All @@ -30,6 +31,42 @@ ruleTester.run("no-invalid-at-rules", rule, {
"@supports (display: grid) { .grid-container { display: grid; } }",
"@namespace url(http://www.w3.org/1999/xhtml);",
"@media screen and (max-width: 600px) { body { font-size: 12px; } }",
{
code: "@foobar url(foo.css) { body { font-size: 12px } }",
languageOptions: {
customSyntax: {
atrules: {
foobar: {
prelude: "<url>",
},
},
},
},
},
{
code: "@tailwind base; @tailwind components; @tailwind utilities;",
languageOptions: {
customSyntax: tailwindSyntax,
},
},
{
code: "a { @apply text-red-500; }",
languageOptions: {
customSyntax: tailwindSyntax,
},
},
{
code: "a { @apply text-red-500 bg-blue-500; }",
languageOptions: {
customSyntax: tailwindSyntax,
},
},
{
code: "@config 'tailwind.config.js';",
languageOptions: {
customSyntax: tailwindSyntax,
},
},
],
invalid: [
{
Expand Down Expand Up @@ -165,5 +202,67 @@ ruleTester.run("no-invalid-at-rules", rule, {
},
],
},
{
code: "@foobar { body { font-size: 12px } }",
languageOptions: {
customSyntax: {
atrules: {
foobar: {
prelude: "<url>",
},
},
},
},
errors: [
{
messageId: "missingPrelude",
data: { name: "foobar" },
line: 1,
column: 1,
endLine: 1,
endColumn: 8,
},
],
},
{
code: "a { @apply; }",
languageOptions: {
customSyntax: tailwindSyntax,
},
errors: [
{
messageId: "missingPrelude",
data: {
name: "apply",
prelude: "",
expected: "<ident>+",
},
line: 1,
column: 5,
endLine: 1,
endColumn: 11,
},
],
},
{
code: "@config;",
languageOptions: {
customSyntax: tailwindSyntax,
},
errors: [
{
messageId: "missingPrelude",
data: {
name: "config",
prelude: "",
expected: "<string>",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 8,
},
],
},
],
});
46 changes: 46 additions & 0 deletions tests/rules/no-invalid-properties.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import rule from "../../src/rules/no-invalid-properties.js";
import css from "../../src/index.js";
import tailwindSyntax from "../../src/syntax/tailwind-syntax.js";
import { RuleTester } from "eslint";

//------------------------------------------------------------------------------
Expand All @@ -32,6 +33,27 @@ ruleTester.run("no-invalid-properties", rule, {
"a { color: red; -moz-transition: bar }",
"@font-face { font-weight: 100 400 }",
'@property --foo { syntax: "*"; inherits: false; }',
{
code: "a { my-custom-color: red; }",
languageOptions: {
customSyntax: {
properties: {
"my-custom-color": "<color>",
},
},
},
},
{
code: "a { my-custom-color: theme(colors.); }",
languageOptions: {
customSyntax: {
properties: {
"my-custom-color": "<color> | <tailwind-theme()>",
},
types: tailwindSyntax.types,
},
},
},
],
invalid: [
{
Expand Down Expand Up @@ -177,5 +199,29 @@ ruleTester.run("no-invalid-properties", rule, {
},
],
},
{
code: "a { my-custom-color: solid; }",
languageOptions: {
customSyntax: {
properties: {
"my-custom-color": "<color>",
},
},
},
errors: [
{
messageId: "invalidPropertyValue",
data: {
property: "my-custom-color",
value: "solid",
expected: "<color>",
},
line: 1,
column: 22,
endLine: 1,
endColumn: 27,
},
],
},
],
});
Loading

0 comments on commit 4e0c041

Please sign in to comment.