diff --git a/docs/adding-languages.md b/docs/adding-languages.md index 8186a5e..74b7e1b 100644 --- a/docs/adding-languages.md +++ b/docs/adding-languages.md @@ -20,7 +20,7 @@ Add an SVG logo into the `public/languages/` directory. 1. Import the type for the new language options from `src/hooks/use-explorer.ts`. 1. Add an entry in the `languages` variable for the new language. 1. Export a new variable describing the available modes for the new language (such as `jsonModes` or `cssModes`). -1. Default the default code for the new language (such as `defaultJsonCode` or `defaultCssCode`). +1. Define the default code for the new language (such as `defaultJsonCode` or `defaultCssCode`). 1. Add an entry in the `defaultCode` variable for the new language's default code. 1. Export a variable containing the default options for the new language (such as `defaultJsonOptions` or `defaultCssOptions`). 1. Add an entry to the `esquerySelectorPlaceholder` constant for the new language(must match Language enum), using an appropriate example selector. @@ -32,32 +32,15 @@ Now import the default options for the new language from `src/lib/const.ts`. ## Step 5: Update `src/components/options.tsx` 1. Import the new language mode type from `src/hooks/use-explorer` (such as `JsonMode` or `CssMode`). -1. Import the available modes for the new language from `src/hooks/const` (such as `jsonModes` or `cssModes`). +1. Import the available modes for the new language from `src/lib/const.ts` (such as `jsonModes` or `cssModes`). 1. Create an options panel for the new language (such as `JsonPanel` or `CssPanel`). 1. Update the `Panel` variable to use the new options panel. -## Step 6: Update `src/components/editor.ts` +## Step 6: Update `src/components/editor.tsx` 1. Install the appropriate CodeMirror plugin for the new language. 1. Update the `languageExtensions` variable to include the new language CodeMirror plugin. -## Step 7: Add AST components +## Step 7: Wire the AST -In the `src/components/ast` directory, create two files: - -1. `{new language}-ast.tsx` -1. `{new language}-ast-tree-item.tsx` - -Replace `{new language}` with the name of the language. You can copy existing files for other languages to get started. - -In `{new language}-ast-tree-item.tsx`, update the name of the options type and the exported component to match the new language. - -Next, install the ESLint language plugin for the new language. - -In `{new language}-ast.tsx`: - -1. Update language references to point to the new language. -1. Import the ESLint language plugin to parse the code. -1. Set the `defaultValue` in the accordion to the name of the root node for the new language AST. - -Last, update `src/components/index.tsx` to import `{new language}-ast.tsx` and update `Ast` to include the new language. +Add a parsing case in `src/hooks/use-ast.ts` for the new language that produces `{ ok: true, ast }`. diff --git a/src/components/ast/css-ast-tree-item.tsx b/src/components/ast/ast-tree-item.tsx similarity index 90% rename from src/components/ast/css-ast-tree-item.tsx rename to src/components/ast/ast-tree-item.tsx index 9541277..0ccffe0 100644 --- a/src/components/ast/css-ast-tree-item.tsx +++ b/src/components/ast/ast-tree-item.tsx @@ -7,18 +7,18 @@ import { TreeEntry } from "../tree-entry"; import type { FC } from "react"; import { mergeClassNames } from "@/lib/utils"; -type ASTNode = { +export type ASTNode = { readonly type: string; readonly [key: string]: unknown; }; -export type CssAstTreeItemProperties = { +export type ASTTreeItemProperties = { readonly index: number; readonly data: ASTNode; readonly esqueryMatchedNodes: ASTNode[]; }; -export const CssAstTreeItem: FC = ({ +export const ASTTreeItem: FC = ({ data, index, esqueryMatchedNodes, diff --git a/src/components/ast/ast-view-mode.tsx b/src/components/ast/ast-view-mode.tsx index a7c4fb1..b427149 100644 --- a/src/components/ast/ast-view-mode.tsx +++ b/src/components/ast/ast-view-mode.tsx @@ -1,43 +1,22 @@ +import type { FC } from "react"; import { useExplorer } from "@/hooks/use-explorer"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { astViewOptions } from "@/lib/const"; -import { mergeClassNames } from "@/lib/utils"; -import type { FC } from "react"; +import { ViewModeToggle } from "@/components/view-mode-toggle"; export const AstViewMode: FC = () => { const { viewModes, setViewModes } = useExplorer(); const { astView } = viewModes; - const handleValueChange = (value: string) => { - if (!value) { - return; - } - - setViewModes({ ...viewModes, astView: value as "tree" | "json" }); - }; - return ( - - {astViewOptions.map(option => ( - - - {option.label} - - ))} - + options={astViewOptions} + onValueChange={value => + setViewModes({ + ...viewModes, + astView: value as "tree" | "json", + }) + } + /> ); }; diff --git a/src/components/ast/css-ast.tsx b/src/components/ast/css-ast.tsx deleted file mode 100644 index 5e02a37..0000000 --- a/src/components/ast/css-ast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Accordion } from "@/components/ui/accordion"; -import { Editor } from "@/components/editor"; -import { useAST } from "@/hooks/use-ast"; -import { useExplorer } from "@/hooks/use-explorer"; -import { - CssAstTreeItem, - type CssAstTreeItemProperties, -} from "./css-ast-tree-item"; -import type { FC } from "react"; -import { parseError } from "@/lib/parse-error"; -import { ErrorState } from "../error-boundary"; - -export const CssAst: 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/html-ast-tree-item.tsx b/src/components/ast/html-ast-tree-item.tsx deleted file mode 100644 index 6e7ee18..0000000 --- a/src/components/ast/html-ast-tree-item.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { TreeEntry } from "../tree-entry"; -import type { FC } from "react"; -import { mergeClassNames } 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 deleted file mode 100644 index dfcdf6e..0000000 --- a/src/components/ast/html-ast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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 3284a50..ba1067e 100644 --- a/src/components/ast/index.tsx +++ b/src/components/ast/index.tsx @@ -1,24 +1,47 @@ -import { useExplorer } from "@/hooks/use-explorer"; -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"; +import { useAST } from "@/hooks/use-ast"; +import { useExplorer } from "@/hooks/use-explorer"; +import { Accordion } from "@/components/ui/accordion"; +import { Editor } from "@/components/editor"; +import { ErrorState } from "@/components/error-boundary"; +import { ASTTreeItem } from "@/components/ast/ast-tree-item"; +import type { ASTNode } from "@/components/ast/ast-tree-item"; +import { parseError } from "@/lib/parse-error"; -export const Ast: FC = () => { - const { language } = useExplorer(); +export const AST: FC = () => { + const result = useAST(); + const { viewModes, language } = useExplorer(); + const { astView } = viewModes; - switch (language) { - case "markdown": - return ; - case "json": - return ; - case "css": - return ; - case "html": - return ; - default: - return ; + if (!result.ok) { + const message = parseError(result.errors[0]); + return ; } + + const ast = JSON.stringify(result.ast, null, 2); + + if (astView === "tree") { + if (result.ast === null) { + return null; + } + + return ( + + + + ); + } + + return ; }; diff --git a/src/components/ast/javascript-ast-tree-item.tsx b/src/components/ast/javascript-ast-tree-item.tsx deleted file mode 100644 index 2019c2a..0000000 --- a/src/components/ast/javascript-ast-tree-item.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { TreeEntry } from "../tree-entry"; -import type { FC } from "react"; -import type * as espree from "espree"; -import { mergeClassNames } from "@/lib/utils"; - -export type JavascriptAstTreeItemProperties = { - readonly index: number; - readonly data: - | ReturnType - | ReturnType["body"][number]; - readonly esqueryMatchedNodes: unknown[]; -}; - -export const JavascriptAstTreeItem: FC = ({ - data, - index, - esqueryMatchedNodes, -}) => { - const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data); - - return ( - - - {data.type} - - -
- {Object.entries(data).map(item => ( - - ))} -
-
-
- ); -}; diff --git a/src/components/ast/javascript-ast.tsx b/src/components/ast/javascript-ast.tsx deleted file mode 100644 index 2cbd449..0000000 --- a/src/components/ast/javascript-ast.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Accordion } from "@/components/ui/accordion"; -import { Editor } from "@/components/editor"; -import { useExplorer } from "@/hooks/use-explorer"; -import { useAST } from "@/hooks/use-ast"; -import { - JavascriptAstTreeItem, - type JavascriptAstTreeItemProperties, -} from "./javascript-ast-tree-item"; -import type { FC } from "react"; -import { parseError } from "@/lib/parse-error"; -import { ErrorState } from "../error-boundary"; - -export const JavascriptAst: FC = () => { - const result = useAST(); - const explorer = useExplorer(); - const { viewModes } = explorer; - const { astView } = viewModes; - - if (!result.ok) { - const message = parseError(result.errors[0]); - return ; - } - - const ast = JSON.stringify(result.ast, null, 2); - - if (astView === "tree") { - if (result.ast === null) { - return null; - } - - return ( - - - - ); - } - - return ; -}; diff --git a/src/components/ast/json-ast-tree-item.tsx b/src/components/ast/json-ast-tree-item.tsx deleted file mode 100644 index 45f0b6c..0000000 --- a/src/components/ast/json-ast-tree-item.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { TreeEntry } from "../tree-entry"; -import type { FC } from "react"; -import { mergeClassNames } from "@/lib/utils"; - -type ASTNode = { - readonly type: string; - readonly [key: string]: unknown; -}; - -export type JsonAstTreeItemProperties = { - readonly index: number; - readonly data: ASTNode; - readonly esqueryMatchedNodes: ASTNode[]; -}; - -export const JsonAstTreeItem: FC = ({ - data, - index, - esqueryMatchedNodes, -}) => { - const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data); - - return ( - - - {data.type} - - -
- {Object.entries(data).map(item => ( - - ))} -
-
-
- ); -}; diff --git a/src/components/ast/json-ast.tsx b/src/components/ast/json-ast.tsx deleted file mode 100644 index 951399b..0000000 --- a/src/components/ast/json-ast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Accordion } from "@/components/ui/accordion"; -import { Editor } from "@/components/editor"; -import { useAST } from "@/hooks/use-ast"; -import { useExplorer } from "@/hooks/use-explorer"; -import { - JsonAstTreeItem, - type JsonAstTreeItemProperties, -} from "./json-ast-tree-item"; -import type { FC } from "react"; -import { parseError } from "@/lib/parse-error"; -import { ErrorState } from "../error-boundary"; - -export const JsonAst: 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/markdown-ast-tree-item.tsx b/src/components/ast/markdown-ast-tree-item.tsx deleted file mode 100644 index f5b9366..0000000 --- a/src/components/ast/markdown-ast-tree-item.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { TreeEntry } from "../tree-entry"; -import type { FC } from "react"; -import { mergeClassNames } from "@/lib/utils"; - -type ASTNode = { - readonly type: string; - readonly [key: string]: unknown; -}; - -export type MarkdownAstTreeItemProperties = { - readonly index: number; - readonly data: ASTNode; - readonly esqueryMatchedNodes: ASTNode[]; -}; - -export const MarkdownAstTreeItem: FC = ({ - data, - index, - esqueryMatchedNodes, -}) => { - const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data); - - return ( - - - {data.type} - - -
- {Object.entries(data).map(item => ( - - ))} -
-
-
- ); -}; diff --git a/src/components/ast/markdown-ast.tsx b/src/components/ast/markdown-ast.tsx deleted file mode 100644 index 01cd440..0000000 --- a/src/components/ast/markdown-ast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Accordion } from "@/components/ui/accordion"; -import { Editor } from "@/components/editor"; -import { useAST } from "@/hooks/use-ast"; -import { useExplorer } from "@/hooks/use-explorer"; -import { - MarkdownAstTreeItem, - type MarkdownAstTreeItemProperties, -} from "./markdown-ast-tree-item"; -import type { FC } from "react"; -import { parseError } from "@/lib/parse-error"; -import { ErrorState } from "../error-boundary"; - -export const MarkdownAst: 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/labeled-select.tsx b/src/components/labeled-select.tsx index b3da7cd..da266a9 100644 --- a/src/components/labeled-select.tsx +++ b/src/components/labeled-select.tsx @@ -25,7 +25,7 @@ interface PanelProps { icon?: boolean; } -const LabeledSelect = (props: PanelProps) => { +export const LabeledSelect = (props: PanelProps) => { const { id, label, @@ -69,5 +69,3 @@ const LabeledSelect = (props: PanelProps) => { ); }; - -export default LabeledSelect; diff --git a/src/components/labeled-switch.tsx b/src/components/labeled-switch.tsx new file mode 100644 index 0000000..4cb5268 --- /dev/null +++ b/src/components/labeled-switch.tsx @@ -0,0 +1,28 @@ +import type { FC } from "react"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; + +type LabeledSwitchProps = { + id: string; + label: string; + checked: boolean; + onCheckedChange: (value: boolean) => void; +}; + +export const LabeledSwitch: FC = ({ + id, + label, + checked, + onCheckedChange, +}) => { + return ( +
+ + +
+ ); +}; diff --git a/src/components/options.tsx b/src/components/options.tsx index e8c92d6..99eee95 100644 --- a/src/components/options.tsx +++ b/src/components/options.tsx @@ -3,7 +3,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Switch } from "@/components/ui/switch"; import { useExplorer } from "@/hooks/use-explorer"; import { jsonModes, @@ -15,11 +14,11 @@ import { versions, templateEngineSyntaxes, } from "@/lib/const"; -import { Button } from "./ui/button"; -import { Label } from "./ui/label"; +import { Button } from "@/components/ui/button"; import type { FC } from "react"; import { Settings } from "lucide-react"; -import LabeledSelect from "./labeled-select"; +import { LabeledSelect } from "@/components/labeled-select"; +import { LabeledSwitch } from "@/components/labeled-switch"; import type { JsonMode, Language, @@ -49,21 +48,17 @@ const JSONPanel: FC = () => { /> {jsonMode === "jsonc" && ( -
- { - setJsonOptions({ - ...jsonOptions, - allowTrailingCommas: value, - }); - }} - /> - -
+ { + setJsonOptions({ + ...jsonOptions, + allowTrailingCommas: value, + }); + }} + /> )} ); @@ -110,16 +105,14 @@ const CssPanel: FC = () => { const { cssOptions, setCssOptions } = explorer; const { tolerant } = cssOptions; return ( -
- { - setCssOptions({ ...cssOptions, tolerant: value }); - }} - /> - -
+ { + setCssOptions({ ...cssOptions, tolerant: value }); + }} + /> ); }; @@ -166,16 +159,14 @@ const JavaScriptPanel: FC = () => { placeholder="ECMAScript Version" /> -
- { - setJsOptions({ ...jsOptions, isJSX: value }); - }} - /> - -
+ { + setJsOptions({ ...jsOptions, isJSX: value }); + }} + /> ); }; @@ -201,21 +192,19 @@ const HTMLPanel: FC = () => { items={templateEngineSyntaxes} placeholder="Template Engine Syntax" /> -
- { - setHtmlOptions({ ...htmlOptions, frontmatter: value }); - }} - /> - -
+ { + setHtmlOptions({ ...htmlOptions, frontmatter: value }); + }} + /> ); }; -const Panel = ({ language }: { language: string }) => { +const Panel: FC<{ language: Language }> = ({ language }) => { switch (language) { case "json": return ; diff --git a/src/components/path/index.tsx b/src/components/path/index.tsx index 3768d85..182a45b 100644 --- a/src/components/path/index.tsx +++ b/src/components/path/index.tsx @@ -6,6 +6,7 @@ import Graphviz from "graphviz-react"; import { generateCodePath } from "@/lib/generate-code-path"; import { parseError } from "@/lib/parse-error"; import useDebouncedEffect from "use-debounced-effect"; +import { ErrorState } from "@/components/error-boundary"; type ParsedResponse = { codePathList: { @@ -70,13 +71,7 @@ export const CodePath: FC = () => { ); if (error) { - return ( -
-
- Error: {error} -
-
- ); + return ; } if (!extracted) { diff --git a/src/components/path/path-view-mode.tsx b/src/components/path/path-view-mode.tsx index f695230..83ec4a2 100644 --- a/src/components/path/path-view-mode.tsx +++ b/src/components/path/path-view-mode.tsx @@ -1,44 +1,23 @@ +import type { FC } from "react"; import { useExplorer } from "@/hooks/use-explorer"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { pathViewOptions } from "@/lib/const"; -import { mergeClassNames } from "@/lib/utils"; -import type { FC } from "react"; +import { ViewModeToggle } from "@/components/view-mode-toggle"; export const PathViewMode: FC = () => { const explorer = useExplorer(); const { viewModes, setViewModes } = explorer; const { pathView } = viewModes; - const handleValueChange = (value: string) => { - if (!value) { - return; - } - - setViewModes({ ...viewModes, pathView: value as "code" | "graph" }); - }; - return ( - - {pathViewOptions.map(option => ( - - - {option.label} - - ))} - + options={pathViewOptions} + onValueChange={value => + setViewModes({ + ...viewModes, + pathView: value as "code" | "graph", + }) + } + /> ); }; diff --git a/src/components/scope/scope-view-mode.tsx b/src/components/scope/scope-view-mode.tsx index ec1e895..e523688 100644 --- a/src/components/scope/scope-view-mode.tsx +++ b/src/components/scope/scope-view-mode.tsx @@ -1,44 +1,25 @@ +import type { FC } from "react"; import { useExplorer } from "@/hooks/use-explorer"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { scopeViewOptions } from "@/lib/const"; -import { mergeClassNames } from "@/lib/utils"; -import type { FC } from "react"; +import { ViewModeToggle } from "@/components/view-mode-toggle"; export const ScopeViewMode: FC = () => { const explorer = useExplorer(); const { viewModes, setViewModes } = explorer; const { scopeView } = viewModes; - const handleValueChange = (value: string) => { - if (!value) { - return; - } - - setViewModes({ ...viewModes, scopeView: value as "nested" | "flat" }); - }; - return ( - - {scopeViewOptions.map(option => ( - - - {option.label} - - ))} - + options={scopeViewOptions} + onValueChange={value => + setViewModes({ + ...viewModes, + scopeView: value as "nested" | "flat", + }) + } + groupClassName="border-card" + itemClassName="border-card" + /> ); }; diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index b0b95bf..38271aa 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,5 +1,5 @@ import { createContext, useContext, useEffect, useState } from "react"; -import { getPreferredColorScheme } from "../lib/utils"; +import { getPreferredColorScheme } from "@/lib/utils"; export type Theme = "dark" | "light" | "system"; diff --git a/src/components/view-mode-toggle.tsx b/src/components/view-mode-toggle.tsx new file mode 100644 index 0000000..f4a8889 --- /dev/null +++ b/src/components/view-mode-toggle.tsx @@ -0,0 +1,57 @@ +import type { FC } from "react"; +import type { LucideIcon } from "lucide-react"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { mergeClassNames } from "@/lib/utils"; + +type ViewModeOption = { + value: string; + label: string; + icon: LucideIcon; +}; + +type ViewModeToggleProps = { + value: string; + options: ViewModeOption[]; + onValueChange: (value: string) => void; + groupClassName?: string; + itemClassName?: string; +}; + +export const ViewModeToggle: FC = ({ + value, + options, + onValueChange, + groupClassName, + itemClassName, +}) => { + const handleValueChange = (val: string) => { + if (!val) return; + onValueChange(val); + }; + + return ( + + {options.map(option => ( + + + {option.label} + + ))} + + ); +}; diff --git a/src/lib/tools.tsx b/src/lib/tools.tsx index 69dae3d..0c27e59 100644 --- a/src/lib/tools.tsx +++ b/src/lib/tools.tsx @@ -1,4 +1,4 @@ -import { Ast } from "@/components/ast"; +import { AST } from "@/components/ast"; import { CodePath } from "@/components/path"; import { Scope } from "@/components/scope"; import { Wrap } from "@/components/wrap"; @@ -12,7 +12,7 @@ export const tools = [ { name: "AST", value: "ast", - component: withErrorBoundary(Ast), + component: withErrorBoundary(AST), options: [Wrap, AstViewMode], }, {