diff --git a/README.md b/README.md index 9b90600..599241d 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,13 @@ The visual JSON editor. Schema-aware, embeddable, extensible. @visual-json/vue Vue UI components (TreeView, FormView, DiffView, and more) + + @visual-json/yaml + YAML support — parse, serialize, and schema-detect YAML files + @visual-json/vscode - VS Code extension — visual JSON editor with tree sidebar, schema support, and JSONC + VS Code extension — visual editor with tree sidebar, schema support, JSONC, and YAML diff --git a/apps/vscode/README.md b/apps/vscode/README.md index d00df25..9679c2c 100644 --- a/apps/vscode/README.md +++ b/apps/vscode/README.md @@ -6,12 +6,13 @@ VS Code extension for [visual-json](https://github.com/vercel-labs/visual-json) ## Features -- **Visual editor** for `.json` and `.jsonc` files via "Open With..." > "visual-json" +- **Visual editor** for `.json`, `.jsonc`, `.yaml`, and `.yml` files via "Open With..." > "visual-json" +- **YAML support** — open and edit YAML files with the same tree and form views used for JSON - **Tree sidebar** with expand/collapse, drag-and-drop, keyboard navigation, and search - **Form editor** with inline editing, schema-aware inputs, and breadcrumb navigation -- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, etc.) +- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, `docker-compose.yml`, GitHub Actions workflows, etc.) - **Theme integration** — adapts to your VS Code color theme automatically -- **Explorer panel** — sidebar view that syncs with the active JSON file +- **Explorer panel** — sidebar view that syncs with the active JSON or YAML file ## Development diff --git a/apps/web/package.json b/apps/web/package.json index d8af029..f52f87b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,7 @@ "@upstash/redis": "^1.36.2", "@visual-json/core": "workspace:*", "@visual-json/react": "workspace:*", + "@visual-json/yaml": "workspace:*", "ai": "^6.0.94", "bash-tool": "^1.3.15", "class-variance-authority": "^0.7.1", diff --git a/apps/web/src/app/docs/changelog/page.mdx b/apps/web/src/app/docs/changelog/page.mdx index a9a7c30..f3d8d59 100644 --- a/apps/web/src/app/docs/changelog/page.mdx +++ b/apps/web/src/app/docs/changelog/page.mdx @@ -2,6 +2,15 @@ export { metadata } from "./metadata"; # Changelog +## 0.4.0 + +### Features + +- **YAML support** — new `@visual-json/yaml` package for parsing, serializing, and round-tripping YAML through the visual-json tree model. The VS Code extension now opens `.yaml` and `.yml` files with the same tree and form views used for JSON. +- **YAML schema detection** — `resolveSchema` in `@visual-json/core` now recognizes well-known YAML filenames (`docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`, etc.) and glob patterns (`.github/workflows/*.yml`, `.github/actions/*/action.yml`) for automatic schema-aware editing. + +--- + ## 0.3.0 ### Features diff --git a/apps/web/src/app/docs/core/page.mdx b/apps/web/src/app/docs/core/page.mdx index 15d31f3..d694b44 100644 --- a/apps/web/src/app/docs/core/page.mdx +++ b/apps/web/src/app/docs/core/page.mdx @@ -85,12 +85,16 @@ console.log(history.canUndo, history.canRedo); ## Schema Resolve JSON Schemas from well-known filenames (e.g. `package.json`, `tsconfig.json`) and -navigate schema properties by path. +navigate schema properties by path. YAML config files are supported too — `resolveSchema` +recognizes filenames like `docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`, +and glob patterns like `.github/workflows/*.yml`. ```ts import { resolveSchema, getPropertySchema, clearSchemaCache } from "@visual-json/core"; const schema = await resolveSchema(json, "package.json"); +// Also works with YAML filenames: +// const schema = await resolveSchema(value, "docker-compose.yml"); if (schema) { const propSchema = getPropertySchema(schema, "$.scripts.build"); diff --git a/apps/web/src/app/docs/getting-started/page.mdx b/apps/web/src/app/docs/getting-started/page.mdx index eff6a4d..13fad96 100644 --- a/apps/web/src/app/docs/getting-started/page.mdx +++ b/apps/web/src/app/docs/getting-started/page.mdx @@ -17,7 +17,8 @@ visual-json is split into focused packages: @visual-json/coreHeadless tree model, operations, schema resolution, diffing, search, and validation @visual-json/reactReact UI components — TreeView, FormView, DiffView, and more @visual-json/vueVue 3 UI components — TreeView, FormView, DiffView, and more - @visual-json/vscodeVS Code extension — open any JSON/JSONC file with a visual editor + @visual-json/yamlYAML support — parse, serialize, and schema-detect YAML files + @visual-json/vscodeVS Code extension — open any JSON, JSONC, or YAML file with a visual editor @@ -92,12 +93,15 @@ visual-json supports multiple ways to view and edit your data: ## VS Code Extension -visual-json is also available as a VS Code extension. Open any `.json` or `.jsonc` file, +visual-json is also available as a VS Code extension. Open any `.json`, `.jsonc`, `.yaml`, or `.yml` file, right-click the tab, and choose **"Open With..." > "visual-json"** to use the visual editor. The extension includes a tree sidebar, form editor, schema support (auto-detected from -`$schema` or known filenames like `package.json` and `tsconfig.json`), and adapts to your -VS Code theme colors automatically. +`$schema` or known filenames like `package.json`, `tsconfig.json`, `docker-compose.yml`, +GitHub Actions workflows, and more), and adapts to your VS Code theme colors automatically. + +YAML files are parsed and serialized transparently — you edit them with the same tree and +form views used for JSON, and changes are saved back as YAML. ## Next Steps diff --git a/apps/web/src/app/editor.tsx b/apps/web/src/app/editor.tsx index 6367927..bebec1b 100644 --- a/apps/web/src/app/editor.tsx +++ b/apps/web/src/app/editor.tsx @@ -3,6 +3,11 @@ import { useState, useCallback, useEffect, useRef } from "react"; import type { JsonValue, JsonSchema } from "@visual-json/core"; import { resolveSchema } from "@visual-json/core"; +import { + isYamlFile, + parseYamlContent, + stringifyYamlContent, +} from "@visual-json/yaml"; import { JsonEditor } from "@visual-json/react"; import { Button } from "@/components/ui/button"; import { @@ -34,6 +39,18 @@ import { X, } from "lucide-react"; +function serializeValue(value: JsonValue, fname: string): string { + return isYamlFile(fname) + ? stringifyYamlContent(value) + : JSON.stringify(value, null, 2); +} + +function parseText(text: string, fname: string): JsonValue { + return isYamlFile(fname) + ? parseYamlContent(text) + : (JSON.parse(text) as JsonValue); +} + type ViewMode = "tree" | "raw"; const VIEW_MODES: { id: ViewMode; label: string }[] = [ @@ -70,7 +87,7 @@ const samples: { name: string; filename: string; data: JsonValue }[] = [ }, { name: "OpenAPI spec", - filename: "openapi.json", + filename: "openapi.yaml", data: { openapi: "3.1.0", info: { @@ -481,7 +498,7 @@ export function Editor({ const [editorShowDescriptions, setEditorShowDescriptions] = useState(false); const [editorShowCounts, setEditorShowCounts] = useState(false); const [rawText, setRawText] = useState( - JSON.stringify(samples[0].data, null, 2), + serializeValue(samples[0].data, samples[0].filename), ); const [rawError, setRawError] = useState(null); const [parseError, setParseError] = useState(null); @@ -506,12 +523,12 @@ export function Editor({ skipRawSync.current = false; return; } - setRawText(JSON.stringify(jsonValue, null, 2)); - }, [jsonValue]); + setRawText(serializeValue(jsonValue, filename)); + }, [jsonValue, filename]); - const loadJson = useCallback((text: string, fname: string) => { + const loadContent = useCallback((text: string, fname: string) => { try { - const parsed = JSON.parse(text); + const parsed = parseText(text, fname); setJsonValue(parsed); setFilename(fname); setActiveSample(fname); @@ -519,7 +536,7 @@ export function Editor({ setRawError(null); setParseError(null); } catch { - setParseError("Invalid JSON"); + setParseError(isYamlFile(fname) ? "Invalid YAML" : "Invalid JSON"); } }, []); @@ -537,24 +554,26 @@ export function Editor({ const handlePaste = useCallback(async () => { try { const text = await navigator.clipboard.readText(); - loadJson(text, "pasted.json"); + loadContent(text, "pasted.json"); } catch { setPasteText(""); setPasteDialogOpen(true); } - }, [loadJson]); + }, [loadContent]); const handlePasteSubmit = useCallback(() => { if (pasteText.trim()) { - loadJson(pasteText, "pasted.json"); + loadContent(pasteText, "pasted.json"); } setPasteDialogOpen(false); setPasteText(""); - }, [pasteText, loadJson]); + }, [pasteText, loadContent]); const handleDownload = useCallback(() => { - const text = JSON.stringify(jsonValue, null, 2); - const blob = new Blob([text], { type: "application/json" }); + const yaml = isYamlFile(filename); + const text = serializeValue(jsonValue, filename); + const mime = yaml ? "text/yaml" : "application/json"; + const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -565,23 +584,32 @@ export function Editor({ const handleCopyJson = useCallback(async () => { try { - await navigator.clipboard.writeText(JSON.stringify(jsonValue, null, 2)); + await navigator.clipboard.writeText(serializeValue(jsonValue, filename)); } catch { // clipboard access may be denied } - }, [jsonValue]); + }, [jsonValue, filename]); - const handleRawChange = useCallback((newText: string) => { - setRawText(newText); - try { - const parsed = JSON.parse(newText); - setRawError(null); - skipRawSync.current = true; - setJsonValue(parsed); - } catch (e) { - setRawError(e instanceof Error ? e.message : "Invalid JSON"); - } - }, []); + const handleRawChange = useCallback( + (newText: string) => { + setRawText(newText); + try { + const parsed = parseText(newText, filename); + setRawError(null); + skipRawSync.current = true; + setJsonValue(parsed); + } catch (e) { + setRawError( + e instanceof Error + ? e.message + : isYamlFile(filename) + ? "Invalid YAML" + : "Invalid JSON", + ); + } + }, + [filename], + ); useEffect(() => { const el = dropRef.current; @@ -606,7 +634,7 @@ export function Editor({ const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") - loadJson(reader.result, file.name); + loadContent(reader.result, file.name); }; reader.readAsText(file); } @@ -619,7 +647,7 @@ export function Editor({ el.removeEventListener("dragleave", handleDragLeave); el.removeEventListener("drop", handleDrop); }; - }, [loadJson]); + }, [loadContent]); return (
- Drop JSON file here + Drop JSON or YAML file here
@@ -681,14 +709,14 @@ export function Editor({ { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") - loadJson(reader.result, file.name); + loadContent(reader.result, file.name); }; reader.readAsText(file); } @@ -712,7 +740,7 @@ export function Editor({ size="icon" className="h-7 w-7" onClick={handlePaste} - title="Paste JSON" + title="Paste" > @@ -730,7 +758,7 @@ export function Editor({ size="icon" className="h-7 w-7" onClick={handleCopyJson} - title="Copy JSON" + title="Copy" > @@ -863,12 +891,12 @@ export function Editor({ - Paste JSON + Paste JSON or YAML