Skip to content

Commit b61d954

Browse files
nzakasamareshsm
andauthored
feat: Add CSS support (eslint#75)
Co-authored-by: Amaresh S M <[email protected]>
1 parent fc1270e commit b61d954

File tree

11 files changed

+269
-6
lines changed

11 files changed

+269
-6
lines changed

docs/adding-languages.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Adding Languages
2+
3+
To add a language to Code Explorer, follow these steps.
4+
5+
## Step 1: Add a logo
6+
7+
Add an SVG logo into the `public/languages/` directory.
8+
9+
## Step 2: Update `src/hooks/use-explorer.ts`
10+
11+
1. Update the `Language` type to contain an entry for the new language.
12+
1. Create a new mode variable that defines the primary parsing mode (such as `jsonMode` or `cssMode`).
13+
1. Update the `Code` type to include an entry for the new language.
14+
1. Create a new options variable that defines the options for the new language (such as `JsonOptions` or `CssOptions`).
15+
1. Update the `ExplorerState` type to include getters and setters for the new language's language options.
16+
1. Update the `useExplorer` variable to include the default options for the new language.
17+
18+
## Step 3: Update `src/lib/const.ts`
19+
20+
1. Import the type for the new language options from `src/hooks/use-explorer.ts`.
21+
1. Add an entry in the `languages` variable for the new language.
22+
1. Export a new variable describing the available modes for the new language (such as `jsonModes` or `cssModes`).
23+
1. Default the default code for the new language (such as `defaultJsonCode` or `defaultCssCode`).
24+
1. Add an entry in the `defaultCode` variable for the new language's default code.
25+
1. Export a variable containing the default options for the new language (such as `defaultJsonOptions` or `defaultCssOptions`).
26+
27+
## Step 4: Update `src/hooks/use-explorer.ts` (yes, again)
28+
29+
Now import the default options for the new language from `src/lib/const.ts`.
30+
31+
## Step 5: Update `src/components/options.tsx`
32+
33+
1. Import the new language mode type from `src/hooks/use-explorer` (such as `JsonMode` or `CssMode`).
34+
1. Import the available modes for the new language from `src/hooks/const` (such as `jsonModes` or `cssModes`).
35+
1. Create an options panel for the new language (such as `JsonPanel` or `CssPanel`).
36+
1. Update the `Panel` variable to use the new options panel.
37+
38+
## Step 6: Update `src/components/editor.ts`
39+
40+
1. Install the appropriate CodeMirror plugin for the new language.
41+
1. Update the `languageExtensions` variable to include the new language CodeMirror plugin.
42+
43+
## Step 7: Add AST components
44+
45+
In the `src/components/ast` directory, create two files:
46+
47+
1. `{new language}-ast.tsx`
48+
1. `{new language}-ast-tree-item.tsx`
49+
50+
Replace `{new language}` with the name of the language. You can copy existing files for other languages to get started.
51+
52+
In `{new language}-ast-tree-item.tsx`, update the name of the options type and the exported component to match the new language.
53+
54+
Next, install the ESLint language plugin for the new language.
55+
56+
In `{new language}-ast.tsx`:
57+
58+
1. Update language references to point to the new language.
59+
1. Import the ESLint language plugin to parse the code.
60+
1. Set the `defaultValue` in the accordion to the name of the root node for the new language AST.
61+
62+
Last, update `src/components/index.tsx` to import `{new language}-ast.tsx` and update `Ast` to include the new language.

package-lock.json

Lines changed: 46 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
"node": ">= 20"
3434
},
3535
"dependencies": {
36+
"@codemirror/lang-css": "^6.3.1",
3637
"@codemirror/lang-javascript": "^6.2.2",
3738
"@codemirror/lang-json": "^6.0.1",
3839
"@codemirror/lang-markdown": "^6.2.5",
3940
"@codemirror/language": "^6.10.3",
41+
"@eslint/css": "^0.1.0",
4042
"@eslint/js": "^9.9.0",
4143
"@eslint/json": "^0.4.1",
4244
"@eslint/markdown": "^6.1.1",

public/languages/css.svg

Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
AccordionContent,
3+
AccordionItem,
4+
AccordionTrigger,
5+
} from "@/components/ui/accordion";
6+
import { TreeEntry } from "../tree-entry";
7+
import type { FC } from "react";
8+
9+
type ASTNode = {
10+
readonly type: string;
11+
readonly [key: string]: unknown;
12+
};
13+
14+
type CssAstTreeItemProperties = {
15+
readonly index: number;
16+
readonly data: ASTNode;
17+
};
18+
19+
export const CssAstTreeItem: FC<CssAstTreeItemProperties> = ({
20+
data,
21+
index,
22+
}) => (
23+
<AccordionItem
24+
value={`${index}-${data.type}`}
25+
className="border border-card rounded-lg overflow-hidden"
26+
>
27+
<AccordionTrigger className="text-sm bg-card px-4 py-3 capitalize">
28+
{data.type}
29+
</AccordionTrigger>
30+
<AccordionContent className="p-4 border-t">
31+
<div className="space-y-1">
32+
{Object.entries(data).map(item => (
33+
<TreeEntry key={item[0]} data={item} />
34+
))}
35+
</div>
36+
</AccordionContent>
37+
</AccordionItem>
38+
);

src/components/ast/css-ast.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import css from "@eslint/css";
2+
import { Accordion } from "@/components/ui/accordion";
3+
import { Editor } from "@/components/editor";
4+
import { useExplorer } from "@/hooks/use-explorer";
5+
import { CssAstTreeItem } from "./css-ast-tree-item";
6+
import type { FC } from "react";
7+
import { parseError } from "@/lib/parse-error";
8+
import { ErrorState } from "../error-boundary";
9+
10+
export const CssAst: FC = () => {
11+
const { code, cssOptions, viewModes } = useExplorer();
12+
const { astView } = viewModes;
13+
const { cssMode } = cssOptions;
14+
const language = css.languages[cssMode];
15+
const result = language.parse({ body: code.css });
16+
17+
if (!result.ok) {
18+
const message = parseError(result.errors[0]);
19+
return <ErrorState message={message} />;
20+
}
21+
22+
const ast = JSON.stringify(result.ast, null, 2);
23+
24+
if (astView === "tree") {
25+
return (
26+
<Accordion
27+
type="multiple"
28+
className="px-8 font-mono space-y-3"
29+
defaultValue={["0-StyleSheet"]}
30+
>
31+
<CssAstTreeItem data={result.ast} index={0} />
32+
</Accordion>
33+
);
34+
}
35+
36+
return <Editor readOnly value={ast} />;
37+
};

src/components/ast/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useExplorer } from "@/hooks/use-explorer";
44
import { JavascriptAst } from "./javascript-ast";
55
import { JsonAst } from "./json-ast";
6+
import { CssAst } from "./css-ast";
67
import { MarkdownAst } from "./markdown-ast";
78
import type { FC } from "react";
89

@@ -14,6 +15,8 @@ export const Ast: FC = () => {
1415
return <MarkdownAst />;
1516
case "json":
1617
return <JsonAst />;
18+
case "css":
19+
return <CssAst />;
1720
default:
1821
return <JavascriptAst />;
1922
}

src/components/editor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CodeMirror from "@uiw/react-codemirror";
66
import { json } from "@codemirror/lang-json";
77
import { javascript } from "@codemirror/lang-javascript";
88
import { markdown } from "@codemirror/lang-markdown";
9+
import { css } from "@codemirror/lang-css";
910
import { EditorView } from "@codemirror/view";
1011
import { EditorState } from "@codemirror/state";
1112
import clsx from "clsx";
@@ -21,6 +22,7 @@ const languageExtensions: Record<string, (isJSX?: boolean) => LanguageSupport> =
2122
javascript: (isJSX: boolean = false) => javascript({ jsx: isJSX }),
2223
json: () => json(),
2324
markdown: () => markdown(),
25+
css: () => css(),
2426
};
2527

2628
type EditorProperties = {

src/components/options.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const MarkdownPanel: React.FC = () => {
6666
);
6767
};
6868

69+
const CssPanel: React.FC = () => {
70+
return null;
71+
};
72+
6973
const JavaScriptPanel = () => {
7074
const explorer = useExplorer();
7175
const { jsOptions, setJsOptions } = explorer;
@@ -129,6 +133,8 @@ const Panel = ({ language }: { language: string }) => {
129133
return <JSONPanel />;
130134
case "markdown":
131135
return <MarkdownPanel />;
136+
case "css":
137+
return <CssPanel />;
132138
default:
133139
return <JavaScriptPanel />;
134140
}

src/hooks/use-explorer.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ import {
1111
defaultJsOptions,
1212
defaultJsonOptions,
1313
defaultMarkdownOptions,
14+
defaultCssOptions,
1415
defaultPathIndex,
1516
defaultViewModes,
1617
} from "../lib/const";
1718
export type SourceType = Exclude<Options["sourceType"], undefined>;
1819
export type Version = Exclude<Options["ecmaVersion"], undefined>;
19-
export type Language = "javascript" | "json" | "markdown";
20+
export type Language = "javascript" | "json" | "markdown" | "css";
2021
export type JsonMode = "json" | "jsonc" | "json5";
2122
export type MarkdownMode = "commonmark" | "gfm";
23+
export type CssMode = "css";
2224

23-
export type Code = { javascript: string; json: string; markdown: string };
25+
export type Code = {
26+
javascript: string;
27+
json: string;
28+
markdown: string;
29+
css: string;
30+
};
2431
export type JsOptions = {
2532
parser: string;
2633
sourceType: SourceType;
@@ -36,6 +43,10 @@ export type MarkdownOptions = {
3643
markdownMode: MarkdownMode;
3744
};
3845

46+
export type CssOptions = {
47+
cssMode: CssMode;
48+
};
49+
3950
export type PathIndex = {
4051
index: number;
4152
indexes: number;
@@ -66,6 +77,9 @@ type ExplorerState = {
6677
markdownOptions: MarkdownOptions;
6778
setMarkdownOptions: (markdownOptions: MarkdownOptions) => void;
6879

80+
cssOptions: CssOptions;
81+
setCssOptions: (cssOptions: CssOptions) => void;
82+
6983
wrap: boolean;
7084
setWrap: (wrap: boolean) => void;
7185

@@ -115,6 +129,9 @@ export const useExplorer = create<ExplorerState>()(
115129
jsonOptions: defaultJsonOptions,
116130
setJsonOptions: jsonOptions => set({ jsonOptions }),
117131

132+
cssOptions: defaultCssOptions,
133+
setCssOptions: cssOptions => set({ cssOptions }),
134+
118135
markdownOptions: defaultMarkdownOptions,
119136
setMarkdownOptions: markdownOptions =>
120137
set({ markdownOptions }),

0 commit comments

Comments
 (0)