diff --git a/package-lock.json b/package-lock.json index 34945cd2..e97ffefb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-markdown": "^6.2.5", @@ -17,6 +18,7 @@ "@eslint/js": "^9.9.0", "@eslint/json": "^0.12.0", "@eslint/markdown": "^6.4.0", + "@html-eslint/eslint-plugin": "^0.40.3", "@lezer/highlight": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.1.1", @@ -1358,6 +1360,39 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@html-eslint/eslint-plugin": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.40.3.tgz", + "integrity": "sha512-ju0BTpo60/NOqh2VIQzBz2d+cekzmvqJZ2BHFRfg3oDQqWzp8nIrgNiKws0ILfs82su/w4zZBqKtMMQp4WvsvQ==", + "dependencies": { + "@eslint/plugin-kit": "0.2.8", + "@html-eslint/parser": "^0.40.0", + "@html-eslint/template-parser": "^0.40.0", + "@html-eslint/template-syntax-parser": "^0.40.0" + } + }, + "node_modules/@html-eslint/parser": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.40.0.tgz", + "integrity": "sha512-xvySQLPogafK2aTOPBD6V7+4qpr1AnOZXO8zM2z0b4i3BB9ISk5PTWu5+ofOekKxjxUh7Z+QWaoezu1SIi33YA==", + "dependencies": { + "@html-eslint/template-syntax-parser": "^0.40.0", + "es-html-parser": "0.2.0" + } + }, + "node_modules/@html-eslint/template-parser": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@html-eslint/template-parser/-/template-parser-0.40.0.tgz", + "integrity": "sha512-f1S70T88yRe3zStT0CiDDgRw24Fgi1wgbW74YNUocEVuXvh2snxBEufY46Yg2udGlOz+SnQaygshacW45L+JWw==", + "dependencies": { + "es-html-parser": "0.2.0" + } + }, + "node_modules/@html-eslint/template-syntax-parser": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@html-eslint/template-syntax-parser/-/template-syntax-parser-0.40.0.tgz", + "integrity": "sha512-Rq3UmjiWreKnXrN95Nt6vLlKgQrV8PhoYrlADn3u7X7vtgCccTmjV6aUHJCw7AyNXmcSNNpN2KPsBmQfH3y58A==" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4658,6 +4693,11 @@ "node": ">= 0.4" } }, + "node_modules/es-html-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.2.0.tgz", + "integrity": "sha512-snJ7uJC8Dkx/yT0eYZrWcY57rkPU6Zui6YphPynw8r52AWf57gjqMC0GWe7OxSDipwXowFpa3rqckEeAPTOz7w==" + }, "node_modules/es-iterator-helpers": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", diff --git a/package.json b/package.json index 4a7e3435..37a62352 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-markdown": "^6.2.5", @@ -43,6 +44,7 @@ "@eslint/js": "^9.9.0", "@eslint/json": "^0.12.0", "@eslint/markdown": "^6.4.0", + "@html-eslint/eslint-plugin": "^0.40.3", "@lezer/highlight": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.1.1", diff --git a/public/languages/html.svg b/public/languages/html.svg new file mode 100644 index 00000000..9a664361 --- /dev/null +++ b/public/languages/html.svg @@ -0,0 +1,7 @@ + + HTML5 Logo Badge + + + + + \ No newline at end of file diff --git a/src/components/ast/html-ast-tree-item.tsx b/src/components/ast/html-ast-tree-item.tsx new file mode 100644 index 00000000..847df108 --- /dev/null +++ b/src/components/ast/html-ast-tree-item.tsx @@ -0,0 +1,52 @@ +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { TreeEntry } from "../tree-entry"; +import type { FC } from "react"; +import { cn } from "@/lib/utils"; + +type ASTNode = { + readonly type: string; + readonly [key: string]: unknown; +}; + +export type HtmlAstTreeItemProperties = { + readonly index: number; + readonly data: ASTNode; + readonly esqueryMatchedNodes: ASTNode[]; +}; + +export const HtmlAstTreeItem: FC = ({ + data, + index, + esqueryMatchedNodes, +}) => { + const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data); + + return ( + + + {data.type} + + +
+ {Object.entries(data).map(item => ( + + ))} +
+
+
+ ); +}; diff --git a/src/components/ast/html-ast.tsx b/src/components/ast/html-ast.tsx new file mode 100644 index 00000000..dfcdf6e5 --- /dev/null +++ b/src/components/ast/html-ast.tsx @@ -0,0 +1,44 @@ +import { Accordion } from "@/components/ui/accordion"; +import { Editor } from "@/components/editor"; +import { useAST } from "@/hooks/use-ast"; +import { useExplorer } from "@/hooks/use-explorer"; +import { + HtmlAstTreeItem, + type HtmlAstTreeItemProperties, +} from "./html-ast-tree-item"; +import type { FC } from "react"; +import { parseError } from "@/lib/parse-error"; +import { ErrorState } from "../error-boundary"; + +export const HtmlAst: FC = () => { + const result = useAST(); + const { viewModes } = useExplorer(); + const { astView } = viewModes; + + if (!result.ok) { + const message = parseError(result.errors[0]); + return ; + } + + const ast = JSON.stringify(result.ast, null, 2); + + if (astView === "tree") { + return ( + + + + ); + } + + return ; +}; diff --git a/src/components/ast/index.tsx b/src/components/ast/index.tsx index d201c9b1..c462512b 100644 --- a/src/components/ast/index.tsx +++ b/src/components/ast/index.tsx @@ -5,6 +5,7 @@ import { JavascriptAst } from "./javascript-ast"; import { JsonAst } from "./json-ast"; import { CssAst } from "./css-ast"; import { MarkdownAst } from "./markdown-ast"; +import { HtmlAst } from "./html-ast"; import type { FC } from "react"; export const Ast: FC = () => { @@ -17,6 +18,8 @@ export const Ast: FC = () => { return ; case "css": return ; + case "html": + return ; default: return ; } diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 71d6d0e2..350271ca 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -7,6 +7,7 @@ import { json } from "@codemirror/lang-json"; import { javascript } from "@codemirror/lang-javascript"; import { markdown } from "@codemirror/lang-markdown"; import { css } from "@codemirror/lang-css"; +import { html } from "@codemirror/lang-html"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import clsx from "clsx"; @@ -27,6 +28,7 @@ const languageExtensions: Record LanguageSupport> = json: () => json(), markdown: () => markdown(), css: () => css(), + html: () => html(), }; type EditorProperties = { diff --git a/src/components/options.tsx b/src/components/options.tsx index 4fff781f..5d04a3bd 100644 --- a/src/components/options.tsx +++ b/src/components/options.tsx @@ -180,6 +180,10 @@ const JavaScriptPanel = () => { ); }; +const HTMLPanel: React.FC = () => { + return <>; +}; + const Panel = ({ language }: { language: string }) => { switch (language) { case "json": @@ -188,6 +192,8 @@ const Panel = ({ language }: { language: string }) => { return ; case "css": return ; + case "html": + return ; default: return ; } diff --git a/src/hooks/use-ast.ts b/src/hooks/use-ast.ts index 0bfd85ae..f771af8c 100644 --- a/src/hooks/use-ast.ts +++ b/src/hooks/use-ast.ts @@ -3,6 +3,7 @@ import type { Node as EstreeNode } from "estree"; import css from "@eslint/css"; import json from "@eslint/json"; import markdown from "@eslint/markdown"; +import html from "@html-eslint/eslint-plugin"; import esquery from "esquery"; import { useExplorer } from "@/hooks/use-explorer"; import { assertIsUnreachable } from "@/lib/utils"; @@ -86,6 +87,17 @@ export function useAST() { break; } + case "html": { + const language = html.languages.html; + astParseResult = language.parse({ + body: code.html, + path: "", + physicalPath: "", + bom: false, + }); + break; + } + default: { assertIsUnreachable(language); } diff --git a/src/hooks/use-explorer.ts b/src/hooks/use-explorer.ts index 4be2dbf3..98547ecd 100644 --- a/src/hooks/use-explorer.ts +++ b/src/hooks/use-explorer.ts @@ -17,7 +17,7 @@ import { } from "../lib/const"; export type SourceType = Exclude; export type Version = Exclude; -export type Language = "javascript" | "json" | "markdown" | "css"; +export type Language = "javascript" | "json" | "markdown" | "css" | "html"; export type JsonMode = "json" | "jsonc" | "json5"; export type MarkdownMode = "commonmark" | "gfm"; export type MarkdownFrontmatter = "off" | "yaml" | "toml"; @@ -28,6 +28,7 @@ export type Code = { json: string; markdown: string; css: string; + html: string; }; export type JsOptions = { parser: string; diff --git a/src/lib/const.ts b/src/lib/const.ts index d462087c..79d2e6f3 100644 --- a/src/lib/const.ts +++ b/src/lib/const.ts @@ -38,6 +38,11 @@ export const languages = [ label: "CSS", icon: "/languages/css.svg", }, + { + value: "html", + label: "HTML", + icon: "/languages/html.svg", + }, ]; export const parsers = [ @@ -376,11 +381,36 @@ p { margin: 1em 0; }`.trim(); +export const defaultHtmlCode = ` + + + + + + + HTML + + +

Text

+ + +`.trim(); + export const defaultCode: Code = { javascript: defaultJsCode, json: defaultJsonCode, markdown: defaultMarkdownCode, css: defaultCssCode, + html: defaultHtmlCode, }; export const defaultJsOptions: JsOptions = { diff --git a/src/lib/convert-nodes-to-ranges.ts b/src/lib/convert-nodes-to-ranges.ts index 75c4d6b3..3d44c045 100644 --- a/src/lib/convert-nodes-to-ranges.ts +++ b/src/lib/convert-nodes-to-ranges.ts @@ -18,6 +18,11 @@ export function convertNodesToRanges( node.loc.start.offset, node.loc.end.offset, ] satisfies HighlightedRange; + } else if (isNodeWithRange(node)) { + return [ + node.range[0], + node.range[1], + ] satisfies HighlightedRange; } }) .filter(range => range !== undefined); @@ -42,6 +47,10 @@ type PositionElem = { offset: number; }; +type NodeWithRange = { + range: [number, number]; +}; + function isNodeWithStartEnd(node: unknown): node is NodeWithStartEnd { return ( typeof node === "object" && @@ -92,3 +101,15 @@ function isNodeWithLoc(node: unknown): node is NodeWithLoc { typeof node.loc.end.offset === "number" ); } + +function isNodeWithRange(node: unknown): node is NodeWithRange { + return ( + typeof node === "object" && + node !== null && + "range" in node && + Array.isArray(node.range) && + node.range.length === 2 && + typeof node.range[0] === "number" && + typeof node.range[1] === "number" + ); +} diff --git a/vite.config.ts b/vite.config.ts index be6ff70f..12d2773b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "src"), }, }, + optimizeDeps: { + include: ["@html-eslint/eslint-plugin"], + }, build: { outDir: "build", },