diff --git a/CHANGELOG.md b/CHANGELOG.md index 991f904..77a8350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.9.2 — 2026-10-27 + +- **Source view.** Every Archify page can show its file's YAML in place: **Diagram | Graph | Source** on diagrams (the old **Diagram | Graph** switch, now on every diagram; Graph only inside a model), **Model | Graph | Source** on `main.archify`, **View | Source** on registry and decision pages, and Source on the Graph page too. The YAML opens in an editor inside the page, under its toolbar and heading, in the page's own colours: line numbers, folding, brackets, multiple cursors, undo and redo, ⌘S/Ctrl+S saves. Problems are underlined as you type, listed under the editor (click the count to go to the next one) and counted on the **Source** segment, also while the picture shows. Completion and hover come from the schema of the file, with nothing else installed. Edits go to the VS Code document, so the tab's dirty dot, save, Git and the text editor stay in step. **Diagram** (or **Model**, **View**) brings the picture back as it was left; anything rendered meanwhile shows then. +- **2D | 3D** now sits left of the view switch (it says how the picture is drawn, the switch what is shown) and hides while Source shows; the 3D stage stops drawing meanwhile. +- **Schema checks without the YAML extension.** `.archify` files are checked against the extension's schema on the YAML as written, at once and with readable messages (`Unknown key "lable" in connections[4].`) on the key or value concerned; `archify validate` runs once the schema is satisfied. `.archify-registry` files are checked against the registry schema for the first time (types, unknown keys, ids). The decision schema is no longer handed to the YAML extension: decision files use the `decision` key twice by design, which it reported as an error in every file. +- **Commands.** *Archify | Show Source* switches the focused page to its Source view. The old behaviour, the YAML in VS Code's text editor beside the page, is *Archify | Open in Text Editor*, which is now the editor-title button. +- Registry and decision pages are text editors of their files (as diagram pages already were), so edits made there make the tab dirty and save with it. +- Fixed: a YAML error in a registry or decision file was reported on its first line instead of where it is. +- Fixed: an unhandled "Webview is disposed" error when a diagram closed while it was rendering. On `main.archify` pages with a diagram, the model heading no longer runs under the toolbar in narrow editors. + ## 0.9.1 — 2026-10-23 - No functional change. Republished so that Open VSX carries the release: the 0.9.0 upload there was held under review while the `root-at-skic` namespace was still unverified, and a held upload is not released by the later ownership grant. diff --git a/README.md b/README.md index b2cb24f..d617942 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Free and open source. If it saves you time, you can [buy me a coffee](https://bu | File | Format | What the extension does | |---|---|---| -| **`name.archify`** | **YAML** (the main format) | Opens as the interactive diagram. Show Source, live updates, validation, completion, templates. | +| **`name.archify`** | **YAML** (the main format) | Opens as the interactive diagram, with a Source view for its YAML. Live updates, validation, completion, templates. | | `name.architecture.json`, `name.workflow.json`, `name.sequence.json`, `name.dataflow.json`, `name.lifecycle.json` | JSON (Archify's own CLI format) | Opens as text, with a preview beside it and validation. For files produced by the Archify CLI or AI agents. | | Rendered Archify `.html` | HTML | Read-only viewer instead of a browser. | @@ -44,7 +44,7 @@ To write one by hand: ``` 2. Click it in the Explorer: the diagram opens. -3. To edit, use **Show Source** in the editor title. The YAML opens beside the diagram, which re-renders as you type. +3. To edit, click **Source** in the diagram's toolbar (**Diagram | Graph | Source**). The YAML takes the picture's place, checked as you type; **Diagram** brings the picture back. To see the text and the picture side by side, use **Open in Text Editor** in the editor title instead: the diagram re-renders as you type. The same file is in [examples/hello.archify](examples/hello.archify). @@ -53,7 +53,7 @@ The same file is in [examples/hello.archify](examples/hello.archify). A `.archify` file is an Archify diagram written in **YAML**. Its fields are those of Archify's diagram schema; the extension converts the YAML for the renderer, so you never handle JSON yourself. `diagram_type` selects the kind of diagram: `architecture`, `workflow`, `sequence`, `dataflow` or `lifecycle`. - **Opening:** the **Archify Diagram** editor is the default for `.archify`. Use *Reopen Editor With… → Text Editor* to edit the YAML full-screen. -- **Completion:** with the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) (`redhat.vscode-yaml`) installed, `.archify` files get completion and hover from Archify's schemas. The schema follows `diagram_type`. +- **Completion:** in the Source view, `.archify` and `.archify-registry` files get completion and hover from Archify's schemas, with nothing else installed; the schema follows `diagram_type`. In VS Code's text editor the same comes from the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) (`redhat.vscode-yaml`), if installed. `.archify` files from extension version 0.2.0 were written in JSON. They still open, because JSON is valid YAML, but new files should use YAML. @@ -66,19 +66,19 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam - **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder. - **New model from an example**: *Archify | New Model…* creates a complete model folder from a C4 example (Standard Web App, E-Shop, Event-Based Integration). - **Model graph**: the **Graph** switch on any page of a model shows the whole model as one graph, with a query box (`type:database`, `uses:api`, `from:web to:db`, …), a side panel per element and PNG export. See [Models](#models). -- **Diagram editor**: `.archify` files open as the rendered diagram. **Show Source** opens the YAML beside it, and edits re-render live. +- **Diagram editor**: `.archify` files open as the rendered diagram. **Source** shows the YAML in its place, in an editor inside the page: problems underlined as you type and counted on the switch, completion and hover from the schema, undo, and ⌘S/Ctrl+S to save. **Open in Text Editor** opens the YAML beside the diagram instead, and edits re-render live. - **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`). - **HTML viewer**: right-click a rendered `.html` file and choose **Archify | Open in HTML Viewer**, or use *Reopen Editor With… → Archify HTML Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk. -- **Problems**: YAML syntax errors, plus `archify validate`'s schema, layout and evidence problems, appear at the matching line of the `.archify` file. +- **Problems**: YAML syntax errors, schema problems (unknown keys, wrong types, missing fields, checked with the extension's own schema check on the YAML as written) and `archify validate`'s layout and evidence problems appear at the matching place in the `.archify` file. Registries are checked against their schema too. - **Brand logos**: after `brand:` the editor suggests Archify's built-in brand IDs (PostgreSQL, Redis, Kafka, Docker, …). Typing an alias or product name finds them too, so `kafka` offers `apache-kafka`. An unknown brand is reported on its own line. -- **Schema help**: completion and hover from Archify's own schemas, through the YAML extension for `.archify` files. +- **Schema help**: completion and hover from Archify's own schemas: in the Source view built in, in the text editor through the YAML extension. - **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead. - **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog. -- **Commands**: *Archify | New Model…*, *Archify | New Registry…*, *Archify | New Diagram…*, *Archify | New Decision…*, *Archify | Open Model*, *Archify | Open Model Graph*, *Archify | Render to HTML File…*, *Archify | Open in Browser*, *Archify | Refresh Preview*, *Archify | Show Source*. +- **Commands**: *Archify | New Model…*, *Archify | New Registry…*, *Archify | New Diagram…*, *Archify | New Decision…*, *Archify | Open Model*, *Archify | Open Model Graph*, *Archify | Render to HTML File…*, *Archify | Open in Browser*, *Archify | Refresh Preview*, *Archify | Show Source* (switches the page to its Source view), *Archify | Open in Text Editor*. ## 3D view -Every diagram can also be seen in 3D. The **2D | 3D** switch next to **Export** in the diagram's toolbar shows which view you are in; click it to flip. +Every diagram can also be seen in 3D. The **2D | 3D** switch in the diagram's toolbar, left of **Diagram | Graph | Source**, shows which view you are in; click it to flip. It hides while Source shows, and **Diagram** returns to the 2D or 3D you left. ![The same diagram in the 3D view](docs/preview-3d.png) @@ -124,10 +124,10 @@ online-shop/ - **Links.** A registry element (or a component) with `diagram: 0300-components/api.archify` points at the diagram that explains it: double-click the node in the 2D view to open it. - **Descriptions.** Under a model diagram, hovering a row of the **Elements** panel shows the registry description of that element. - **Decisions.** One `*.archify-decision` file per architecture decision: id, title, status, date, the registry elements and diagrams it is about, and prose for context, decision and consequences. Diagrams point at decisions with `decisions: []` on the diagram, a component or a connection. They show as a **Decisions** panel on the `main.archify` page, a **Decisions** rail panel under each diagram (hover lights up what the decision is attached to), on element cards and in the element dialog, and a decision opens as its own page. *Archify | New Decision…* creates one; links are checked in Problems. -- **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **Show Source** opens the YAML beside it. +- **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **View | Source** at the top right switches to the YAML, checked against the registry schema as you type. - **Elements and Connectors.** Under every diagram, Archify's node index is titled **Elements**, and a **Connectors** panel next to it lists every connection as "from → to" with its label. Hovering highlights the connector in the diagram; clicking keeps it highlighted and scrolls to it. - **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`. -- **The graph.** The **Model | Graph** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or 0) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph** and opens the graph already narrowed to that diagram. *Archify | Open Model Graph* does the same from the Command Palette or Explorer. +- **The graph.** The **Model | Graph | Source** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or 0) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph | Source** and Graph opens the graph already narrowed to that diagram; **Source** on the graph page goes back to `main.archify` in its Source view. *Archify | Open Model Graph* does the same from the Command Palette or Explorer. - **Queries.** Type a few words in the box (/ focuses it); matches stay bright, the rest dims, the count shows "9 of 14 elements". Terms combine with AND and a leading `-` excludes: `database` (id, label, sublabel, brand or type contain it), `type:backend`, `diagram:0200-*` (what those diagrams draw), `decision:` (what a decision is about or is attached to), `uses:api` (an element and its neighbours), `near:api,2` (within two steps), `from:web to:db` (the shortest path, following the arrows first), `unused` (registry elements no diagram draws), `shared` and `-shared` (registry or inline elements only), `type:decision` and `type:registry` for the decision and registry nodes. Decisions and registries follow the elements they are attached to into a result; `-type:decision` or `-type:registry` leaves them out. Walks and paths follow the diagrams' connections only, so a registry is never a shortcut between its elements. The **?** button lists them; clicking an example runs it. - **Side panel.** Click an element for its description, type, where it is defined (**Registry** opens the file at the line), the diagrams that draw it (click to open), its connections and its decisions (click to open). **Neighbours** runs `uses:` for it. Click a connection for its labels, the diagrams that draw it and its decisions. Click a decision node for its status, context and decision text, the elements and connections it is about, and **Open decision**; a registry node for the elements it defines, the ones no diagram draws yet, and **Open registry**. - **Commands.** *Archify | Open Model* opens the model page of the current file, from the editor title, the Command Palette or the Explorer menu of a `.archify` or `.archify-registry` file; *Archify | Open Model Graph* opens the graph. diff --git a/esbuild.mjs b/esbuild.mjs index fc5d2ab..5e80f0f 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -18,6 +18,17 @@ const common = { mainFields: ['module', 'main'], }; +// codemirror-json-schema formats schema descriptions with markdown-it and +// shiki (about 400 KB); a small stub does what our descriptions need. +const stubMarkdown = { + name: 'stub-markdown', + setup(build) { + build.onResolve({ filter: /utils\/markdown(\.js)?$/ }, (args) => + args.importer.includes('codemirror-json-schema') ? { path: path.resolve('src/webview/source/markdownStub.ts') } : undefined, + ); + }, +}; + if (tests) { // Unit tests cover the modules that do not import 'vscode'; the // integration suite runs inside VS Code, which provides it. @@ -58,6 +69,16 @@ if (tests) { entryPoints: ['src/graph/index.ts'], outfile: 'dist/graph.js', }), + // The Source view; CodeMirror is bundled in. Loaded the first time Source is chosen. + esbuild.context({ + ...common, + format: 'iife', + platform: 'browser', + target: 'es2022', + entryPoints: ['src/webview/source/index.ts'], + outfile: 'dist/source.js', + plugins: [stubMarkdown], + }), ]); if (watch) { await Promise.all(contexts.map((ctx) => ctx.watch())); diff --git a/package-lock.json b/package-lock.json index 31bff43..adb747f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,25 @@ { "name": "archify-vscode-ext", - "version": "0.9.1", + "version": "0.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "archify-vscode-ext", - "version": "0.9.1", + "version": "0.9.2", "license": "MIT", "dependencies": { + "@codemirror/autocomplete": "^6.20.4", + "@codemirror/commands": "^6.11.1", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.13.1", + "@codemirror/lint": "^6.9.7", + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.15", + "@lezer/highlight": "^1.2.5", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "codemirror-json-schema": "^0.8.1", "cytoscape": "^3.34.4", "cytoscape-fcose": "^2.2.0", "three": "^0.186.1", @@ -22,7 +33,6 @@ "@types/vscode": "~1.100.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", - "ajv": "^8.20.0", "esbuild": "^0.25.0", "jsonc-parser": "^3.3.1", "typescript": "^5.8.0" @@ -252,6 +262,120 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.4", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.4.tgz", + "integrity": "sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz", + "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz", + "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/streamparser": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz", + "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", + "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/streamparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz", + "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==", + "license": "MIT" + }, + "node_modules/@codemirror/view": { + "version": "6.43.15", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz", + "integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -701,6 +825,59 @@ "node": ">=18" } }, + "node_modules/@lezer/common": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.4.tgz", + "integrity": "sha512-xt3Q4z64xM+xhg4M7NXqw1N//1O74bUFo5kz6fwMeC4qwxSuRdwkYSmjcqCEEKYkNw5e67f2hcNqBoNERAEU0w==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz", + "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.11.tgz", + "integrity": "sha512-UwgZcl1R66H2hDzBrA7kWyYmX6s8p5TQ+e/t7CuvgCtBEPxbEPaHYHu+pJBvTvDotEJpuQkJjbXuHiw5Cixxgw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz", + "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -739,6 +916,22 @@ "node": ">= 8" } }, + "node_modules/@sagold/json-pointer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@sagold/json-pointer/-/json-pointer-5.1.2.tgz", + "integrity": "sha512-+wAhJZBXa6MNxRScg6tkqEbChEHMgVZAhTHVJ60Y7sbtXtu9XA49KfUkdWlS2x78D6H9nryiKePiYozumauPfA==", + "license": "MIT" + }, + "node_modules/@sagold/json-query": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sagold/json-query/-/json-query-6.2.0.tgz", + "integrity": "sha512-7bOIdUE6eHeoWtFm8TvHQHfTVSZuCs+3RpOKmZCDBIOrxpvF/rNFTeuvIyjHva/RR0yVS3kQtr+9TW72LQEZjA==", + "license": "MIT", + "dependencies": { + "@sagold/json-pointer": "^5.1.2", + "ebnf": "^1.9.1" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -913,6 +1106,85 @@ "node": ">=20.0.0" } }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/markdown-it": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.29.2.tgz", + "integrity": "sha512-RPHqGU8RGQZ2TGMnEqLnSyM9CjPSjb0f8bwSLnJgBmWPWguoygoaFyYkXG0kwMtBtChNYsqQz1C0fLcbo6dY8g==", + "license": "MIT", + "dependencies": { + "markdown-it": "^14.1.0", + "shiki": "1.29.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", + "license": "MIT" + }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -1036,6 +1308,24 @@ "cytoscape": "^3.31" } }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "22.20.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", @@ -1082,6 +1372,12 @@ "meshoptimizer": "~1.1.1" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.100.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", @@ -1111,6 +1407,12 @@ "node": ">=22.0.0" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@vscode/test-electron": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", @@ -1334,7 +1636,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1396,7 +1697,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/astral-regex": { @@ -1459,6 +1759,12 @@ "license": "MIT", "optional": true }, + "node_modules/best-effort-json-parser": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/best-effort-json-parser/-/best-effort-json-parser-1.5.1.tgz", + "integrity": "sha512-Snlb3OTA7lnOW1vZyPByO/cWl3wDElisGId4BIEhmPGOWJaEyGpXoRgaIFXDIRh59NmIiuTY2p0YPq89sfwjWw==", + "license": "BSD-2-Clause" + }, "node_modules/binaryextensions": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", @@ -1618,6 +1924,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1635,6 +1951,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -1726,6 +2062,68 @@ "node": ">=16" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-json-schema": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/codemirror-json-schema/-/codemirror-json-schema-0.8.1.tgz", + "integrity": "sha512-4lKPjW+nugNAmM5MsggJyn6TUxYdCCwAJIr9T4cZeTFPdkbBvPteCOGtDedrTOIeTC2ZFJtVg7VHIXnYU32t8w==", + "license": "MIT", + "dependencies": { + "@sagold/json-pointer": "^5.1.1", + "@shikijs/markdown-it": "^1.22.2", + "best-effort-json-parser": "^1.1.2", + "json-schema": "^0.4.0", + "json-schema-library": "^9.3.5", + "loglevel": "^1.9.1", + "markdown-it": "^14.1.0", + "shiki": "^1.22.2", + "yaml": "^2.3.4" + }, + "optionalDependencies": { + "@codemirror/autocomplete": "^6.16.2", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.1", + "codemirror-json5": "^1.0.3", + "json5": "^2.2.3" + }, + "peerDependencies": { + "@codemirror/language": "^6.10.2", + "@codemirror/lint": "^6.8.0", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.2.1" + } + }, + "node_modules/codemirror-json5": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/codemirror-json5/-/codemirror-json5-1.0.3.tgz", + "integrity": "sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "json5": "^2.2.1", + "lezer-json5": "^2.0.2" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1759,6 +2157,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -1785,6 +2193,12 @@ "layout-base": "^2.0.0" } }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1882,6 +2296,15 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-browser": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", @@ -1935,6 +2358,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1946,6 +2378,25 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2020,6 +2471,15 @@ "node": ">= 0.4" } }, + "node_modules/ebnf": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ebnf/-/ebnf-1.9.1.tgz", + "integrity": "sha512-uW2UKSsuty9ANJ3YByIQE4ANkD8nqUPO7r6Fwcc1ADKPe9FRdcPpMl3VEput4JSvKBJ4J86npIC2MLP0pYkCuw==", + "license": "MIT", + "bin": { + "ebnf": "dist/bin.js" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2054,6 +2514,12 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -2083,7 +2549,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -2207,11 +2672,16 @@ "node": ">=6" } }, + "node_modules/fast-copy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.1.0.tgz", + "integrity": "sha512-IuCOCDFM1F/s4LtG7Bjr3yUUwPHybyhqN74xk2cMYoP51vVeErMy8+6HGFchi/+45lW8uzhs6lBeEqks2GWt6A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2235,7 +2705,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", - "dev": true, "funding": [ { "type": "github", @@ -2512,6 +2981,42 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -2525,6 +3030,16 @@ "node": ">=10" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -2841,18 +3356,38 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-library": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.3.5.tgz", + "integrity": "sha512-5eBDx7cbfs+RjylsVO+N36b0GOPtv78rfqgf2uON+uaHUIC62h63Y8pkV2ovKbaL4ZpQcHp21968x5nx/dFwqQ==", + "license": "MIT", + "dependencies": { + "@sagold/json-pointer": "^5.1.2", + "@sagold/json-query": "^6.1.3", + "deepmerge": "^4.3.1", + "fast-copy": "^3.0.2", + "fast-deep-equal": "^3.1.3", + "smtp-address-parser": "1.0.10", + "valid-url": "^1.0.9" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -3002,6 +3537,16 @@ "node": ">=6" } }, + "node_modules/lezer-json5": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lezer-json5/-/lezer-json5-2.0.2.tgz", + "integrity": "sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@lezer/lr": "^1.0.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -3016,7 +3561,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "dev": true, "funding": [ { "type": "github", @@ -3138,6 +3682,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3155,7 +3712,6 @@ "version": "14.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", - "dev": true, "funding": [ { "type": "github", @@ -3189,11 +3745,31 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdurl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -3213,6 +3789,95 @@ "dev": true, "license": "MIT" }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3335,6 +4000,12 @@ "license": "MIT", "optional": true }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "license": "BSD-3-Clause" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3357,6 +4028,34 @@ "license": "MIT", "optional": true }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "license": "MIT", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -3481,6 +4180,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -3786,6 +4496,16 @@ "dev": true, "license": "MIT" }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -3802,7 +4522,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3846,6 +4565,25 @@ ], "license": "MIT" }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "license": "CC0-1.0" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "license": "MIT", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3938,11 +4676,35 @@ "node": ">= 6" } }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3965,6 +4727,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4093,6 +4864,22 @@ "dev": true, "license": "MIT" }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -4262,6 +5049,28 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smtp-address-parser": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz", + "integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==", + "license": "MIT", + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -4360,6 +5169,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -4397,6 +5220,12 @@ "boundary": "^2.0.0" } }, + "node_modules/style-mod": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", + "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4568,6 +5397,16 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4642,7 +5481,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, "license": "MIT" }, "node_modules/underscore": { @@ -4682,6 +5520,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz", + "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -4706,6 +5612,11 @@ "dev": true, "license": "MIT" }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -4730,6 +5641,40 @@ "url": "https://bevry.me/fund" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -4846,6 +5791,16 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 96346c6..b344f5d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "archify-vscode-ext", "displayName": "Archify Diagram Viewer", "description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.", - "version": "0.9.1", + "version": "0.9.2", "publisher": "root-at-skic", "license": "MIT", "sponsor": { @@ -102,6 +102,11 @@ { "command": "archify.showSource", "title": "Archify | Show Source", + "icon": "$(code)" + }, + { + "command": "archify.openInTextEditor", + "title": "Archify | Open in Text Editor", "icon": "$(go-to-file)" }, { @@ -145,6 +150,10 @@ "command": "archify.showSource", "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer' || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision'" }, + { + "command": "archify.openInTextEditor", + "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer' || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision'" + }, { "command": "archify.openModel", "when": "archify.isDiagramSource || archifyPreviewFocus || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision'" @@ -171,7 +180,7 @@ "group": "navigation" }, { - "command": "archify.showSource", + "command": "archify.openInTextEditor", "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer' || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision'", "group": "navigation" }, @@ -454,10 +463,6 @@ { "fileMatch": "*.archify-registry", "url": "./schemas/archify-registry.schema.json" - }, - { - "fileMatch": "*.archify-decision", - "url": "./schemas/archify-decision.schema.json" } ] }, @@ -479,7 +484,6 @@ "@types/vscode": "~1.100.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", - "ajv": "^8.20.0", "esbuild": "^0.25.0", "jsonc-parser": "^3.3.1", "typescript": "^5.8.0" @@ -489,6 +493,17 @@ }, "homepage": "https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext#readme", "dependencies": { + "@codemirror/autocomplete": "^6.20.4", + "@codemirror/commands": "^6.11.1", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.13.1", + "@codemirror/lint": "^6.9.7", + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.15", + "@lezer/highlight": "^1.2.5", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "codemirror-json-schema": "^0.8.1", "cytoscape": "^3.34.4", "cytoscape-fcose": "^2.2.0", "three": "^0.186.1", diff --git a/src/commands/viewCommands.ts b/src/commands/viewCommands.ts index a066574..6de8e7d 100644 --- a/src/commands/viewCommands.ts +++ b/src/commands/viewCommands.ts @@ -39,7 +39,17 @@ export function viewCommands(ctx: CommandContext): vscode.Disposable[] { vscode.commands.registerCommand('archify.refreshPreview', () => ctx.views.activePreview?.update(true)), + // On a page that has a Source view, show it; elsewhere open the text. vscode.commands.registerCommand('archify.showSource', async () => { + const session = ctx.views.activePreview?.source ?? ctx.registry.activeView?.source ?? ctx.decision.activeView?.source; + if (session) { + session.reveal(); + return; + } + await vscode.commands.executeCommand('archify.openInTextEditor'); + }), + + vscode.commands.registerCommand('archify.openInTextEditor', async () => { const preview = ctx.views.activePreview; if (preview?.isEditor) { // The diagram is the editor; open the YAML text beside it. diff --git a/src/extension.ts b/src/extension.ts index 36c94e6..cd58e34 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,8 +22,8 @@ export function activate(context: vscode.ExtensionContext): void { extension: context, views: new DiagramViewManager(context, models), htmlViewer: new HtmlViewerProvider(context.extensionUri), - registry: new RegistryViewProvider(models), - decision: new DecisionViewProvider(models), + registry: new RegistryViewProvider(context, models), + decision: new DecisionViewProvider(context, models), models, }; const refs = new RefLanguage(models); diff --git a/src/graph/index.ts b/src/graph/index.ts index b10ed6e..4293169 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -226,12 +226,27 @@ class GraphPage { this.nameEl.textContent = this.graph.meta.name || this.graph.meta.id || 'Untitled model'; titles.append(this.kickerEl, this.nameEl); const actions = el('div', 'ag-actions'); - const sw = el('button', 'ag-switch'); - sw.type = 'button'; - sw.title = 'Back to the model page'; - sw.setAttribute('aria-label', 'Graph view on; switch to the model page'); - sw.append(el('span', 'ag-seg', 'Model'), el('span', 'ag-seg on', 'Graph')); - sw.addEventListener('click', () => this.host.post({ type: 'showModel' })); + // Model | Graph | Source, as on the model page. Source goes back to the + // model page and opens it in Source (the page reads sourceView on load). + const sw = el('div', 'ag-switch'); + sw.setAttribute('role', 'group'); + sw.setAttribute('aria-label', 'View'); + const segment = (label: string, title: string, on: boolean, action?: () => void) => { + const button = el('button', `ag-seg${on ? ' on' : ''}`, label); + button.type = 'button'; + button.title = title; + button.setAttribute('aria-pressed', String(on)); + if (action) button.addEventListener('click', action); + return button; + }; + sw.append( + segment('Model', 'Back to the model page', false, () => this.host.post({ type: 'showModel' })), + segment('Graph', 'The model as one graph', true), + segment('Source', 'Edit the YAML of main.archify, checked as you type', false, () => { + this.host.setState({ sourceView: true }); + this.host.post({ type: 'showModel' }); + }), + ); const exportButton = el('button', 'ag-button ag-primary', 'Export PNG'); exportButton.type = 'button'; exportButton.title = 'Save the graph as it is on screen, as a PNG'; diff --git a/src/graph/style.ts b/src/graph/style.ts index 57d6a97..1a4bfc2 100644 --- a/src/graph/style.ts +++ b/src/graph/style.ts @@ -20,13 +20,14 @@ html, body { margin: 0; height: 100%; overflow: hidden; background: var(--ag-bg) .ag-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; } .ag-bar .ag-button { min-height: 2rem; } /* Toolbar controls follow Archify's: quiet pills (0.5rem) inside 0.5625rem buttons, one filled primary action (0.6875rem), the 2D | 3D switch's twin. */ -.ag-switch { position: relative; isolation: isolate; display: inline-flex; padding: 0 0.25rem; border: 0; border-radius: 0.5625rem; background: transparent; color: var(--ag-muted); font: inherit; cursor: pointer; } +.ag-switch { position: relative; isolation: isolate; display: inline-flex; padding: 0 0.25rem; border: 0; border-radius: 0.5625rem; background: transparent; color: var(--ag-muted); font: inherit; } .ag-switch::before { content: ""; position: absolute; z-index: -1; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--ag-panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ag-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); } .ag-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ag-fg) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); } -.ag-switch:focus-visible { outline: 2px solid var(--ag-accent); outline-offset: -4px; } -.ag-seg { display: inline-flex; align-items: center; height: 1.75rem; margin: 0.25rem 0; padding: 0 0.7rem; border-radius: 0.5rem; font-weight: 550; font-size: 0.8125rem; transition: background 0.15s, color 0.15s; } +.ag-seg { display: inline-flex; align-items: center; height: 1.75rem; margin: 0.25rem 0; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; } +.ag-seg.on { cursor: default; } +.ag-seg:focus-visible { outline: 2px solid var(--ag-accent); outline-offset: 1px; } .ag-seg.on { color: var(--ag-bg); background: var(--ag-fg); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); } -.ag-switch:hover .ag-seg:not(.on) { color: var(--ag-fg); } +.ag-seg:hover:not(.on) { color: var(--ag-fg); } .ag-button { position: relative; isolation: isolate; display: inline-flex; align-items: center; gap: 0.4rem; font: inherit; font-size: 0.8125rem; font-weight: 500; min-height: 2.25rem; padding: 0.25rem 0.58rem; border-radius: 0.5625rem; border: 0; background: transparent; color: var(--ag-muted); cursor: pointer; white-space: nowrap; transition: color 0.15s; } .ag-button::before { content: ""; position: absolute; z-index: -1; inset: 0.25rem 0; border-radius: 0.5rem; background: transparent; transition: background 0.15s, box-shadow 0.15s; } .ag-button:hover { color: var(--ag-fg); } diff --git a/src/language/diagnostics.ts b/src/language/diagnostics.ts index 5b2725d..bab778d 100644 --- a/src/language/diagnostics.ts +++ b/src/language/diagnostics.ts @@ -10,6 +10,7 @@ import { isArchifyFile } from '../sources/detect'; import { log } from '../util/log'; import path from 'node:path'; import { diagnosticPointer, rangeForPointer, rangeForPointerYaml, yamlSyntaxProblems } from './jsonPointer'; +import { DIAGRAM_SCHEMA, REGISTRY_SCHEMA, SchemaChecker, SchemaProblem, readSchemas } from './schemaCheck'; const JSON_LANGUAGES = new Set(['json', 'jsonc']); @@ -33,6 +34,7 @@ export class DiagnosticsController implements vscode.Disposable { private readonly timers = new Map(); private readonly sequence = new Map(); private readonly disposables: vscode.Disposable[]; + private checker: SchemaChecker | undefined; constructor( private readonly context: vscode.ExtensionContext, @@ -54,6 +56,22 @@ export class DiagnosticsController implements vscode.Disposable { this.revalidateAll(); } + /** Our schemas, compiled on first use. */ + private schemas(): SchemaChecker { + this.checker ??= new SchemaChecker(readSchemas(vscode.Uri.joinPath(this.context.extensionUri, 'schemas').fsPath)); + return this.checker; + } + + private schemaDiagnostic(doc: vscode.TextDocument, problem: SchemaProblem): vscode.Diagnostic { + const diagnostic = new vscode.Diagnostic( + new vscode.Range(doc.positionAt(problem.from), doc.positionAt(problem.to)), + problem.message, + vscode.DiagnosticSeverity.Error, + ); + diagnostic.source = 'archify'; + return diagnostic; + } + private enabled(uri: vscode.Uri): boolean { return vscode.workspace.getConfiguration('archify', uri).get('validation.enabled', true); } @@ -136,6 +154,17 @@ export class DiagnosticsController implements vscode.Disposable { } } + if (isArchifyFile(doc.fileName)) { + // The schema check sees the source as written and answers at once; a + // file that fails it would fail archify validate the same way, so that + // runs only on files the schema accepts. + const schemaProblems = this.schemas().check(text, DIAGRAM_SCHEMA); + if (schemaProblems.length) { + this.collection.set(doc.uri, [...modelDiagnostics, ...schemaProblems.map((problem) => this.schemaDiagnostic(doc, problem))]); + return; + } + } + try { const runtime = await runtimeFor(this.context, doc.uri); const result = await validate(runtime, source.type, source.json); @@ -210,6 +239,10 @@ export class DiagnosticsController implements vscode.Disposable { const root = await this.models.rootOf(doc.uri); const rel = root ? path.relative(root.fsPath, doc.uri.fsPath) : path.basename(doc.uri.fsPath); const problems: ModelProblem[] = [...parseRegistry(text, rel).problems]; + // The schema adds what the parser does not check (types, unknown keys, + // ids); a file the parser already rejects as a whole gets no second opinion. + const shapeless = problems.some((p) => /not valid YAML|must be a mapping|needs an "elements" mapping/.test(p.message)); + const schemaProblems = shapeless ? [] : this.schemas().check(text, REGISTRY_SCHEMA); if (root) { try { const index = await this.models.index(root); @@ -223,7 +256,10 @@ export class DiagnosticsController implements vscode.Disposable { problems.push({ message: `This registry is not inside a model: no main.archify above ${path.basename(doc.uri.fsPath)}.`, severity: 'warning', line: 0 }); } if (seq !== this.sequence.get(doc.uri.toString()) || doc.isClosed) return; - this.collection.set(doc.uri, problems.map((problem) => this.modelDiagnostic(doc, text, 'yaml', problem))); + this.collection.set(doc.uri, [ + ...problems.map((problem) => this.modelDiagnostic(doc, text, 'yaml', problem)), + ...schemaProblems.map((problem) => this.schemaDiagnostic(doc, problem)), + ]); } dispose(): void { diff --git a/src/language/editorSchemas.ts b/src/language/editorSchemas.ts new file mode 100644 index 0000000..a6f08e6 --- /dev/null +++ b/src/language/editorSchemas.ts @@ -0,0 +1,85 @@ +import { DIAGRAM_SCHEMA, REGISTRY_SCHEMA } from './schemaCheck'; + +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +/** + * Schemas for completion and hover in the Source view. The editor's schema + * library reads one draft-07 document, while ours are 2020-12 files that + * refer to each other; so the referenced files are folded into `$defs` and + * 2020-12 tuples become draft-07 ones. Problems never come from these + * copies (the host checks with the originals), only suggestions. + */ +export class EditorSchemas { + private readonly cache = new Map(); + + constructor(private readonly schemas: Record) {} + + /** The schema for a file kind; for diagrams the one of its diagram_type. */ + forSource(kind: 'diagram' | 'registry' | 'decision', text: string): { key: string; schema: object } | undefined { + if (kind === 'decision') return undefined; + if (kind === 'registry') return this.get(REGISTRY_SCHEMA); + const type = /^diagram_type:\s*["']?([a-z]+)/m.exec(text)?.[1]; + const name = type && this.schemas[`${type}.schema.json`] ? `${type}.schema.json` : DIAGRAM_SCHEMA; + return this.get(name); + } + + private get(name: string): { key: string; schema: object } | undefined { + if (!this.schemas[name]) return undefined; + if (!this.cache.has(name)) this.cache.set(name, bundleSchema(this.schemas, name)); + return { key: name, schema: this.cache.get(name)! }; + } +} + +const defKey = (file: string) => `file:${file}`; + +/** One self-contained draft-07 shaped schema for `root` and every file it refers to. */ +export function bundleSchema(schemas: Record, root: string): object { + const files = new Set([root]); + const queue = [root]; + while (queue.length) { + const file = queue.shift()!; + for (const ref of refsOf(schemas[file] as Json)) { + const target = ref.split('#')[0]; + if (target && !files.has(target) && schemas[target]) { + files.add(target); + queue.push(target); + } + } + } + const base = (file: string) => (file === root ? '#' : `#/$defs/${defKey(file)}`); + const rewrite = (node: Json, file: string): Json => { + if (Array.isArray(node)) return node.map((item) => rewrite(item, file)); + if (!node || typeof node !== 'object') return node; + const out: { [key: string]: Json } = {}; + for (const [key, value] of Object.entries(node)) { + if (key === '$ref' && typeof value === 'string') { + const [target, pointer = ''] = value.split('#'); + out.$ref = `${base(target || file)}${pointer}`; + } else if (key !== '$schema' && key !== '$id') { + out[key] = rewrite(value, file); + } + } + if (out.prefixItems) { + if (out.items === false) out.additionalItems = false; + else if (out.items !== undefined) out.additionalItems = out.items; + out.items = out.prefixItems; + delete out.prefixItems; + } + return out; + }; + const bundled = rewrite(schemas[root] as Json, root) as { [key: string]: Json }; + const defs = { ...((bundled.$defs as { [key: string]: Json } | undefined) ?? {}) }; + for (const file of files) if (file !== root) defs[defKey(file)] = rewrite(schemas[file] as Json, file); + return { ...bundled, $defs: defs }; +} + +function refsOf(node: Json, out: string[] = []): string[] { + if (Array.isArray(node)) node.forEach((item) => refsOf(item, out)); + else if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node)) { + if (key === '$ref' && typeof value === 'string') out.push(value); + else refsOf(value, out); + } + } + return out; +} diff --git a/src/language/schemaCheck.ts b/src/language/schemaCheck.ts new file mode 100644 index 0000000..bd28930 --- /dev/null +++ b/src/language/schemaCheck.ts @@ -0,0 +1,148 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import Ajv2020, { ErrorObject, ValidateFunction } from 'ajv/dist/2020'; +import { LineCounter, Node, isMap, isScalar, isSeq, parseDocument } from 'yaml'; + +/** A schema problem placed on the YAML node it is about; offsets into the text. */ +export interface SchemaProblem { + from: number; + to: number; + message: string; + /** 'yaml' for a syntax error (only the first is reported), 'schema' otherwise. */ + kind: 'yaml' | 'schema'; +} + +/** Schema file names in schemas/ that a file kind is checked against. */ +export const DIAGRAM_SCHEMA = 'archify.schema.json'; +export const REGISTRY_SCHEMA = 'archify-registry.schema.json'; + +/** Reads every *.schema.json of a folder: file name → schema. */ +export function readSchemas(folder: string): Record { + return Object.fromEntries( + fs + .readdirSync(folder) + .filter((name) => name.endsWith('.schema.json')) + .map((name) => [name, JSON.parse(fs.readFileSync(path.join(folder, name), 'utf8')) as object]), + ); +} + +/** + * Checks YAML text against our JSON Schemas (draft 2020-12, with ajv), so the + * Source view and the Problems panel see the same problems whether or not a + * YAML extension is installed. Schemas refer to each other by file name. + */ +export class SchemaChecker { + private readonly ajv = new Ajv2020({ allErrors: true, strict: false }); + private readonly compiled = new Map(); + + constructor(schemas: Record) { + for (const [name, schema] of Object.entries(schemas)) this.ajv.addSchema(schema, name); + } + + private validator(name: string): ValidateFunction | undefined { + if (!this.compiled.has(name)) this.compiled.set(name, this.ajv.getSchema(name)); + return this.compiled.get(name); + } + + check(text: string, schema: string): SchemaProblem[] { + const doc = parseDocument(text, { lineCounter: new LineCounter(), prettyErrors: false, uniqueKeys: false }); + if (doc.errors.length) { + // Later syntax errors are usually echoes of the first. + const error = doc.errors[0]; + return [{ from: error.pos[0], to: Math.max(error.pos[1], error.pos[0] + 1), message: `YAML: ${error.message.split('\n')[0]}`, kind: 'yaml' }]; + } + const validate = this.validator(schema); + if (!validate || validate(doc.toJS())) return []; + return collapse(validate.errors ?? []).map((error) => { + const [from, to] = rangeFor(doc.contents, error, text.length); + return { from, to, message: describe(error), kind: 'schema' as const }; + }); + } +} + +/** oneOf/anyOf/if produce a cascade; keep the errors a person can act on. */ +function collapse(errors: ErrorObject[]): ErrorObject[] { + const seen = new Set(); + return errors + .filter((e) => !['if', 'oneOf', 'anyOf', 'allOf'].includes(e.keyword)) + .filter((e) => { + const key = `${e.instancePath}|${e.keyword}|${JSON.stringify(e.params)}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +/** "/connections/4/label" → "connections[4].label". */ +export function readablePointer(pointer: string): string { + if (!pointer) return 'the file'; + return pointer + .split('/') + .slice(1) + .map((part) => (/^\d+$/.test(part) ? `[${part}]` : `.${part.replace(/~1/g, '/').replace(/~0/g, '~')}`)) + .join('') + .replace(/^\./, ''); +} + +function describe(e: ErrorObject): string { + const where = readablePointer(e.instancePath); + const params = e.params as Record; + switch (e.keyword) { + case 'additionalProperties': + return `Unknown key "${params.additionalProperty}" in ${where}.`; + case 'required': + return `"${params.missingProperty}" is required in ${where}.`; + case 'enum': + return `${where} must be one of: ${(params.allowedValues as unknown[]).join(', ')}.`; + case 'const': + return `${where} must be ${JSON.stringify(params.allowedValue)}.`; + case 'type': { + const type = String(params.type); + return `${where} must be ${type === 'array' ? 'a list' : type === 'object' ? 'a mapping' : `a ${type}`}.`; + } + case 'minItems': + case 'maxItems': { + const limit = Number(params.limit); + return `${where} needs ${e.keyword === 'minItems' ? 'at least' : 'at most'} ${limit} item${limit === 1 ? '' : 's'}.`; + } + case 'propertyNames': + return `"${params.propertyName}" in ${where} is not a valid id: start with a letter; letters, digits, - and _ after.`; + default: + return `${where} ${e.message ?? 'is not valid'}.`; + } +} + +function keyText(key: unknown): string { + return isScalar(key) ? String(key.value) : String(key); +} + +/** The node an error is about: the offending key for unknown keys, else the value. */ +function rangeFor(root: Node | null, error: ErrorObject, length: number): [number, number] { + const parts = error.instancePath.split('/').slice(1).map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~')); + let node: unknown = root; + let keyNode: Node | null = null; + for (const part of parts) { + if (isMap(node)) { + const pair = node.items.find((p) => keyText(p.key) === part); + if (!pair) break; + keyNode = pair.key as Node; + node = pair.value; + } else if (isSeq(node)) { + keyNode = null; + node = node.items[Number(part)]; + } else break; + } + const params = error.params as Record; + const extra = params.additionalProperty ?? params.propertyName; + if (extra !== undefined && isMap(node)) { + const pair = node.items.find((p) => keyText(p.key) === String(extra)); + const range = (pair?.key as Node | undefined)?.range; + if (range) return [range[0], range[1]]; + } + if (error.keyword === 'required' && keyNode?.range) return [keyNode.range[0], keyNode.range[1]]; + const target = (node as Node | undefined)?.range ? (node as Node) : keyNode; + if (!target?.range) return [0, Math.min(1, length)]; + // A block mapping or list spans many lines; mark its first character only. + if ((isMap(target) || isSeq(target)) && !target.flow) return [target.range[0], target.range[0] + 1]; + return [target.range[0], Math.max(target.range[1], target.range[0] + 1)]; +} diff --git a/src/model/decisions.ts b/src/model/decisions.ts index d223526..c49b812 100644 --- a/src/model/decisions.ts +++ b/src/model/decisions.ts @@ -1,4 +1,4 @@ -import { isMap, isScalar, isSeq, parseDocument } from 'yaml'; +import { LineCounter, isMap, isScalar, isSeq, parseDocument } from 'yaml'; import { ModelProblem, Registry } from './format'; /** @@ -46,9 +46,11 @@ export function slugify(title: string): string { /** Parses one decision file. Problems carry 0-based lines. */ export function parseDecision(text: string, file: string): { decision?: Decision; problems: ModelProblem[] } { const problems: ModelProblem[] = []; - const doc = parseDocument(text, { prettyErrors: false, uniqueKeys: false }); + // prettyErrors would put line numbers into the messages; the line counter gives them without. + const lines = new LineCounter(); + const doc = parseDocument(text, { prettyErrors: false, uniqueKeys: false, lineCounter: lines }); for (const error of doc.errors) { - problems.push({ message: `Decision is not valid YAML: ${error.message.split('\n')[0]}`, severity: 'error', file, line: error.linePos?.[0].line ? error.linePos[0].line - 1 : 0 }); + problems.push({ message: `Decision is not valid YAML: ${error.message.split('\n')[0]}`, severity: 'error', file, line: lines.linePos(error.pos[0]).line - 1 }); } if (doc.errors.length) return { problems }; const lineOf = (offset: number | undefined) => (offset === undefined ? 0 : text.slice(0, offset).split('\n').length - 1); diff --git a/src/model/format.ts b/src/model/format.ts index 0941f0e..1fa1bee 100644 --- a/src/model/format.ts +++ b/src/model/format.ts @@ -1,4 +1,4 @@ -import { isMap, isScalar, parseDocument, parse as parseYaml } from 'yaml'; +import { LineCounter, isMap, isScalar, parse as parseYaml, parseDocument } from 'yaml'; /** * The model file formats (see docs/model.md): `main.archify` with a `model` @@ -83,9 +83,11 @@ export function hasDiagramBody(doc: unknown): boolean { export function parseRegistry(text: string, file: string): { elements: RegistryElement[]; problems: ModelProblem[] } { const problems: ModelProblem[] = []; const elements: RegistryElement[] = []; - const doc = parseDocument(text, { prettyErrors: false, uniqueKeys: false }); + // prettyErrors would put line numbers into the messages; the line counter gives them without. + const lines = new LineCounter(); + const doc = parseDocument(text, { prettyErrors: false, uniqueKeys: false, lineCounter: lines }); for (const error of doc.errors) { - problems.push({ message: `Registry is not valid YAML: ${error.message.split('\n')[0]}`, severity: 'error', file, line: error.linePos?.[0].line ? error.linePos[0].line - 1 : 0 }); + problems.push({ message: `Registry is not valid YAML: ${error.message.split('\n')[0]}`, severity: 'error', file, line: lines.linePos(error.pos[0]).line - 1 }); } if (doc.errors.length) return { elements, problems }; const lineOf = (offset: number | undefined) => (offset === undefined ? 0 : text.slice(0, offset).split('\n').length - 1); diff --git a/src/util/textDiff.ts b/src/util/textDiff.ts new file mode 100644 index 0000000..8b3801e --- /dev/null +++ b/src/util/textDiff.ts @@ -0,0 +1,64 @@ +// Text helpers shared by the host and the Source view in the page. The page's +// editor always works in "\n" text; documents may use "\r\n". + +export interface TextChange { + from: number; + to: number; + insert: string; +} + +/** The text with "\r\n" turned into "\n". */ +export function normalizeEol(text: string): string { + return text.replace(/\r\n/g, '\n'); +} + +/** + * The smallest single change that turns `before` into `after`: the common + * start and end are kept, so a cursor outside the change stays put. + */ +export function minimalChange(before: string, after: string): TextChange | undefined { + if (before === after) return undefined; + let start = 0; + const max = Math.min(before.length, after.length); + while (start < max && before.charCodeAt(start) === after.charCodeAt(start)) start++; + let endBefore = before.length; + let endAfter = after.length; + while (endBefore > start && endAfter > start && before.charCodeAt(endBefore - 1) === after.charCodeAt(endAfter - 1)) { + endBefore--; + endAfter--; + } + return { from: start, to: endBefore, insert: after.slice(start, endAfter) }; +} + +/** Line and character of an offset into "\n" text. */ +export function positionOf(text: string, offset: number): { line: number; character: number } { + const clamped = Math.max(0, Math.min(offset, text.length)); + let line = 0; + let lineStart = 0; + for (let i = text.indexOf('\n'); i !== -1 && i < clamped; i = text.indexOf('\n', i + 1)) { + line++; + lineStart = i + 1; + } + return { line, character: clamped - lineStart }; +} + +/** Offset into "\n" text of a line and character (clamped to the line). */ +export function offsetOf(text: string, line: number, character: number): number { + let start = 0; + for (let l = 0; l < line; l++) { + const next = text.indexOf('\n', start); + if (next === -1) return text.length; + start = next + 1; + } + const end = text.indexOf('\n', start); + return Math.min(start + Math.max(0, character), end === -1 ? text.length : end); +} + +/** Applies changes given in the coordinates of `text` (as one editor transaction reports them). */ +export function applyChanges(text: string, changes: TextChange[]): string { + let out = text; + for (const change of [...changes].sort((a, b) => b.from - a.from)) { + out = out.slice(0, change.from) + change.insert + out.slice(change.to); + } + return out; +} diff --git a/src/viewer3d/index.ts b/src/viewer3d/index.ts index acaa640..140a4f4 100644 --- a/src/viewer3d/index.ts +++ b/src/viewer3d/index.ts @@ -58,13 +58,20 @@ class Viewer3D { this.interceptViewerControls(); if (this.saved.active ?? host.defaultView === '3d') this.setActive(true); else this.syncButtons(); + // The Source view hides the picture; the stage stops drawing meanwhile. + window.addEventListener('archify-source', (event) => { + if (!this.active || !this.stage) return; + if ((event as CustomEvent<{ on: boolean }>).detail.on) this.stage.stop(); + else this.stage.start(); + }); } // -------------------------------------------------------------- chrome /** - * The 2D | 3D switch: Export's twin in the toolbar. Both states are always - * visible, the filled half is the current one, a click flips it. + * The 2D | 3D switch in the toolbar, left of Diagram | Graph | Source (it + * says how the picture is drawn, that one what is shown). Both states are + * always visible, the filled half is the current one, a click flips it. */ private buildSwitch(): HTMLButtonElement { const button = document.createElement('button'); @@ -79,8 +86,8 @@ class Viewer3D { event.stopPropagation(); this.setActive(!this.active); }); - const exportWrap = document.querySelector('.toolbar .export-wrap'); - if (exportWrap?.parentElement) exportWrap.parentElement.insertBefore(button, exportWrap); + const anchor = document.querySelector('.toolbar #archify-mode-switch') ?? document.querySelector('.toolbar .export-wrap'); + if (anchor?.parentElement) anchor.parentElement.insertBefore(button, anchor); else document.querySelector('.toolbar')?.appendChild(button); return button; } diff --git a/src/views/decisionView.ts b/src/views/decisionView.ts index 28196c8..3bc6c6a 100644 --- a/src/views/decisionView.ts +++ b/src/views/decisionView.ts @@ -6,31 +6,41 @@ import { DecisionPanel, decisionPanelFromIndex, decisionPanelStandalone } from ' import { ModelService } from '../model/service'; import { readText } from '../util/files'; import { log } from '../util/log'; +import { sourceScriptUri } from '../webview/viewer3d'; +import { SourceSession } from './sourceSession'; import { handleWebviewMessage, WebviewMessage } from '../webview/messages'; import { decisionPage, makeNonce } from '../webview/page'; import { DECISION_VIEW_TYPE, DIAGRAM_EDITOR_VIEW_TYPE, REGISTRY_VIEW_TYPE } from './viewTypes'; export { DECISION_VIEW_TYPE }; -class DecisionDocument implements vscode.CustomDocument { - constructor(readonly uri: vscode.Uri) {} - dispose(): void {} -} - class DecisionView implements vscode.Disposable { private seq = 0; private timer: NodeJS.Timeout | undefined; private last: string | undefined; root: vscode.Uri | undefined; private readonly disposables: vscode.Disposable[] = []; + /** The Source view; while it shows, a new page waits in `heldHtml`. */ + readonly source: SourceSession; + private heldHtml: string | undefined; constructor( + private readonly context: vscode.ExtensionContext, private readonly models: ModelService, readonly panel: vscode.WebviewPanel, readonly uri: vscode.Uri, ) { + this.source = new SourceSession(context, panel, uri, 'decision', (shown) => { + if (!shown && this.heldHtml !== undefined) { + const html = this.heldHtml; + this.heldHtml = undefined; + this.panel.webview.html = html; + } + }); this.disposables.push( + this.source, panel.webview.onDidReceiveMessage((message: WebviewMessage) => { + if (this.source.handle(message)) return; if (message.type === 'openDiagram') return void this.openInModel(message.path, DIAGRAM_EDITOR_VIEW_TYPE); if (message.type === 'openRegistry') return void this.openRegistryLine(message.file, message.line); if (message.type === 'openDecision') return void this.openInModel(message.file, DECISION_VIEW_TYPE); @@ -86,7 +96,12 @@ class DecisionView implements vscode.Disposable { const fingerprint = JSON.stringify(decision); if (fingerprint === this.last) return; this.last = fingerprint; - this.panel.webview.html = decisionPage({ cspSource: this.panel.webview.cspSource, nonce: makeNonce() }, decision); + const html = decisionPage( + { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), source: { scriptUri: sourceScriptUri(this.panel.webview, this.context.extensionUri), left: 'View' } }, + decision, + ); + if (this.source.shown) this.heldHtml = html; + else this.panel.webview.html = html; } dispose(): void { @@ -95,13 +110,16 @@ class DecisionView implements vscode.Disposable { } } -/** Read-only custom editor for .archify-decision files: the decision as a page, with its elements and diagrams. */ -export class DecisionViewProvider implements vscode.CustomReadonlyEditorProvider, vscode.Disposable { +/** Custom editor for .archify-decision files: the decision as a page, with its elements and diagrams. */ +export class DecisionViewProvider implements vscode.CustomTextEditorProvider, vscode.Disposable { private readonly views = new Set(); private active: DecisionView | undefined; private readonly disposables: vscode.Disposable[]; - constructor(private readonly models: ModelService) { + constructor( + private readonly context: vscode.ExtensionContext, + private readonly models: ModelService, + ) { this.disposables = [ vscode.window.registerCustomEditorProvider(DECISION_VIEW_TYPE, this, { webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true }, @@ -122,13 +140,16 @@ export class DecisionViewProvider implements vscode.CustomReadonlyEditorProvider return this.active?.uri; } - openCustomDocument(uri: vscode.Uri): DecisionDocument { - return new DecisionDocument(uri); + /** The view of the focused page, if any. */ + get activeView(): { source: SourceSession } | undefined { + return this.active; } - async resolveCustomEditor(document: DecisionDocument, panel: vscode.WebviewPanel): Promise { + // A text editor provider: the page edits the file's own TextDocument (in + // its Source view), so dirty state, save and undo are VS Code's. + async resolveCustomTextEditor(document: vscode.TextDocument, panel: vscode.WebviewPanel): Promise { panel.webview.options = { enableScripts: true }; - const view = new DecisionView(this.models, panel, document.uri); + const view = new DecisionView(this.context, this.models, panel, document.uri); this.views.add(view); const onViewState = () => { if (panel.active) this.active = view; diff --git a/src/views/diagramView.ts b/src/views/diagramView.ts index f6c23a8..d0a8c25 100644 --- a/src/views/diagramView.ts +++ b/src/views/diagramView.ts @@ -13,7 +13,8 @@ import { log } from '../util/log'; import { viewerTheme } from '../util/settings'; import { handleWebviewMessage, WebviewMessage } from '../webview/messages'; import { graphPage, makeNonce, messagePage, modelPage, prepareDiagramHtml } from '../webview/page'; -import { graphScriptUri, viewer3dOption, viewerRoots } from '../webview/viewer3d'; +import { graphScriptUri, sourceScriptUri, viewer3dOption, viewerRoots } from '../webview/viewer3d'; +import { SourceSession } from './sourceSession'; import { DECISION_VIEW_TYPE, DIAGRAM_EDITOR_VIEW_TYPE, PREVIEW_VIEW_TYPE, REGISTRY_VIEW_TYPE } from './viewTypes'; @@ -50,6 +51,10 @@ class DiagramPreview implements vscode.Disposable { private graphQuery: string | undefined; /** True while the webview shows the Graph page, which takes model updates as messages. */ private graphShown = false; + /** The Source view of a .archify file; while it shows, new HTML waits in `heldHtml`. */ + readonly source: SourceSession | undefined; + private heldHtml: string | undefined; + private disposed = false; constructor( private readonly context: vscode.ExtensionContext, @@ -69,9 +74,22 @@ class DiagramPreview implements vscode.Disposable { panel.title = `Preview ${path.basename(uri.path)}`; } + if (isArchifyFile(uri.path)) { + this.source = new SourceSession(context, panel, uri, 'diagram', (shown) => { + // Back from Source: show what was rendered meanwhile. + if (!shown && this.heldHtml !== undefined) { + const html = this.heldHtml; + this.heldHtml = undefined; + this.panel.webview.html = html; + } + }); + this.disposables.push(this.source); + } + this.disposables.push( panel.onDidDispose(() => this.dispose()), panel.webview.onDidReceiveMessage((message: WebviewMessage) => { + if (this.source?.handle(message)) return; if (message.type === 'hash') { this.hash = message.hash; return; @@ -121,6 +139,22 @@ class DiagramPreview implements vscode.Disposable { Promise.resolve(this.panel.webview.postMessage(message)).catch(() => undefined); } + /** Replaces the page, unless Source is open in it: then the page waits until it closes. */ + private setHtml(html: string): void { + if (this.disposed) return; + if (this.source?.shown) { + this.heldHtml = html; + return; + } + this.heldHtml = undefined; + this.panel.webview.html = html; + } + + /** Page option for the Source view, on .archify files. */ + private sourceOption(left: 'Model' | 'Diagram'): { scriptUri: string; left: 'Model' | 'Diagram' } | undefined { + return this.source ? { scriptUri: sourceScriptUri(this.panel.webview, this.context.extensionUri), left } : undefined; + } + private state(): PreviewState { return { uri: this.uri.toString(), hash: this.hash, graph: this.graph }; } @@ -156,21 +190,21 @@ class DiagramPreview implements vscode.Disposable { return; } this.graphShown = true; - this.panel.webview.html = graphPage( + this.setHtml(graphPage( { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state() }, { graph, query: this.graphQuery, scriptUri: graphScriptUri(this.panel.webview, this.context.extensionUri) }, - ); + )); this.graphQuery = undefined; } private showMessage(title: string, body: string, detail?: string): void { this.lastHtml = undefined; - this.panel.webview.html = messagePage( - { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state() }, + this.setHtml(messagePage( + { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state(), source: this.sourceOption('Diagram') }, title, body, detail, - ); + )); } /** Opens a diagram of the model, given its path relative to the model root. */ @@ -192,7 +226,7 @@ class DiagramPreview implements vscode.Disposable { this.lastHtml = fingerprint; this.rendered = false; this.graphShown = false; - this.panel.webview.html = modelPage({ cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state() }, model); + this.setHtml(modelPage({ cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state(), source: this.sourceOption('Model') }, model)); } scheduleUpdate(): void { @@ -203,6 +237,7 @@ class DiagramPreview implements vscode.Disposable { async update(force = false): Promise { clearTimeout(this.timer); + if (this.disposed) return; const seq = ++this.seq; let text: string; try { @@ -285,6 +320,7 @@ class DiagramPreview implements vscode.Disposable { decisions: prepared.decisions, model, graphSwitch: main ? { left: 'Model' } : prepared.root ? { left: 'Diagram', query: `diagram:${path.relative(prepared.root.fsPath, this.uri.fsPath).split(path.sep).join('/')}` } : undefined, + source: this.sourceOption(main ? 'Model' : 'Diagram'), }); // Nonces differ per render, so compare the renderer output instead. const fingerprint = `${viewerTheme()}\u0000${result.evidenceNotice ?? ''}\u0000${JSON.stringify([model ?? null, [...prepared.links], [...prepared.descriptions], prepared.decisions])}\u0000${result.html}`; @@ -294,10 +330,14 @@ class DiagramPreview implements vscode.Disposable { } this.lastHtml = fingerprint; this.rendered = true; - this.panel.webview.html = html; + this.setHtml(html); } dispose(): void { + if (this.disposed) return; + this.disposed = true; + // Renders in flight check the sequence after every await; this ends them. + this.seq++; clearTimeout(this.timer); this.onDisposeEmitter.fire(); this.onDisposeEmitter.dispose(); diff --git a/src/views/registryView.ts b/src/views/registryView.ts index fb3a7dc..decd7ba 100644 --- a/src/views/registryView.ts +++ b/src/views/registryView.ts @@ -5,31 +5,41 @@ import { RegistryPanel, registryPanelFromIndex, registryPanelStandalone } from ' import { ModelService } from '../model/service'; import { readText } from '../util/files'; import { log } from '../util/log'; +import { sourceScriptUri } from '../webview/viewer3d'; +import { SourceSession } from './sourceSession'; import { handleWebviewMessage, WebviewMessage } from '../webview/messages'; import { makeNonce, registryPage } from '../webview/page'; import { DECISION_VIEW_TYPE, DIAGRAM_EDITOR_VIEW_TYPE, REGISTRY_VIEW_TYPE } from './viewTypes'; export { REGISTRY_VIEW_TYPE }; -class RegistryDocument implements vscode.CustomDocument { - constructor(readonly uri: vscode.Uri) {} - dispose(): void {} -} - class RegistryView implements vscode.Disposable { private seq = 0; private timer: NodeJS.Timeout | undefined; private last: string | undefined; root: vscode.Uri | undefined; private readonly disposables: vscode.Disposable[] = []; + /** The Source view; while it shows, a new page waits in `heldHtml`. */ + readonly source: SourceSession; + private heldHtml: string | undefined; constructor( + private readonly context: vscode.ExtensionContext, private readonly models: ModelService, readonly panel: vscode.WebviewPanel, readonly uri: vscode.Uri, ) { + this.source = new SourceSession(context, panel, uri, 'registry', (shown) => { + if (!shown && this.heldHtml !== undefined) { + const html = this.heldHtml; + this.heldHtml = undefined; + this.panel.webview.html = html; + } + }); this.disposables.push( + this.source, panel.webview.onDidReceiveMessage((message: WebviewMessage) => { + if (this.source.handle(message)) return; if (message.type === 'openDiagram') return void this.openInModel(message.path, DIAGRAM_EDITOR_VIEW_TYPE); if (message.type === 'openRegistry') return void this.openRegistryLine(message.file, message.line); if (message.type === 'openDecision') return void this.openInModel(message.file, DECISION_VIEW_TYPE); @@ -84,7 +94,12 @@ class RegistryView implements vscode.Disposable { const fingerprint = JSON.stringify(registry); if (fingerprint === this.last) return; this.last = fingerprint; - this.panel.webview.html = registryPage({ cspSource: this.panel.webview.cspSource, nonce: makeNonce() }, registry); + const html = registryPage( + { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), source: { scriptUri: sourceScriptUri(this.panel.webview, this.context.extensionUri), left: 'View' } }, + registry, + ); + if (this.source.shown) this.heldHtml = html; + else this.panel.webview.html = html; } dispose(): void { @@ -93,12 +108,15 @@ class RegistryView implements vscode.Disposable { } } -export class RegistryViewProvider implements vscode.CustomReadonlyEditorProvider, vscode.Disposable { +export class RegistryViewProvider implements vscode.CustomTextEditorProvider, vscode.Disposable { private readonly views = new Set(); private active: RegistryView | undefined; private readonly disposables: vscode.Disposable[]; - constructor(private readonly models: ModelService) { + constructor( + private readonly context: vscode.ExtensionContext, + private readonly models: ModelService, + ) { this.disposables = [ vscode.window.registerCustomEditorProvider(REGISTRY_VIEW_TYPE, this, { webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true }, @@ -121,13 +139,16 @@ export class RegistryViewProvider implements vscode.CustomReadonlyEditorProvider return this.active?.uri; } - openCustomDocument(uri: vscode.Uri): RegistryDocument { - return new RegistryDocument(uri); + /** The view of the focused page, if any. */ + get activeView(): { source: SourceSession } | undefined { + return this.active; } - async resolveCustomEditor(document: RegistryDocument, panel: vscode.WebviewPanel): Promise { + // A text editor provider: the page edits the file's own TextDocument (in + // its Source view), so dirty state, save and undo are VS Code's. + async resolveCustomTextEditor(document: vscode.TextDocument, panel: vscode.WebviewPanel): Promise { panel.webview.options = { enableScripts: true }; - const view = new RegistryView(this.models, panel, document.uri); + const view = new RegistryView(this.context, this.models, panel, document.uri); this.views.add(view); const onViewState = () => { if (panel.active) this.active = view; diff --git a/src/views/sourceSession.ts b/src/views/sourceSession.ts new file mode 100644 index 0000000..25a6d9e --- /dev/null +++ b/src/views/sourceSession.ts @@ -0,0 +1,196 @@ +import * as vscode from 'vscode'; +import { EditorSchemas } from '../language/editorSchemas'; +import { readSchemas } from '../language/schemaCheck'; +import { log } from '../util/log'; +import { TextChange, normalizeEol, offsetOf, positionOf } from '../util/textDiff'; + +/** Page → host messages of the Source view. */ +export type SourceMessage = + | { type: 'sourceReady' } + | { type: 'sourceOpen' } + | { type: 'sourceClose' } + | { type: 'sourceEdit'; base: string; text: string; changes: TextChange[] }; + +export type SourceKind = 'diagram' | 'registry' | 'decision'; + +const CHECKS: Record = { + diagram: 'YAML · checked against the Archify schema and the model', + registry: 'YAML · checked against the registry schema and the model', + decision: 'YAML · checked as a decision of the model', +}; + +let editorSchemas: EditorSchemas | undefined; + +function schemasFor(context: vscode.ExtensionContext): EditorSchemas { + editorSchemas ??= new EditorSchemas(readSchemas(vscode.Uri.joinPath(context.extensionUri, 'schemas').fsPath)); + return editorSchemas; +} + +export function isSourceMessage(message: { type?: string }): message is SourceMessage { + return message.type === 'sourceReady' || message.type === 'sourceOpen' || message.type === 'sourceClose' || message.type === 'sourceEdit'; +} + +/** + * The host half of a page's Source view. The document stays VS Code's: page + * edits become WorkspaceEdits (so dirty state, save, undo and Git work as + * for any file), and changes from anywhere else (undo, the text editor, git) + * go back to the page. Problems are whatever the Problems panel holds for the + * file, sent as offsets; the page shows their count on the Source switch even + * while the picture is shown. + */ +export class SourceSession implements vscode.Disposable { + /** True while the page shows Source; the view must not replace its HTML then. */ + shown = false; + /** Texts our own edits will produce, oldest first; their change events are not echoed. */ + private readonly expected: string[] = []; + private queue: Promise = Promise.resolve(); + private schemaKey: string | undefined; + private readonly disposables: vscode.Disposable[]; + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly panel: vscode.WebviewPanel, + readonly uri: vscode.Uri, + readonly kind: SourceKind, + /** Called when Source opens or closes, so the view can hold or apply its HTML. */ + private readonly onShown: (shown: boolean) => void, + ) { + const same = (uri: vscode.Uri) => uri.toString() === this.uri.toString(); + this.disposables = [ + vscode.languages.onDidChangeDiagnostics((event) => { + if (event.uris.some(same)) void this.postDiagnostics(); + }), + vscode.workspace.onDidChangeTextDocument((event) => { + if (!same(event.document.uri) || !event.contentChanges.length) return; + this.post({ type: 'sourceDirty', dirty: event.document.isDirty }); + if (this.shown) this.syncFrom(event.document); + }), + vscode.workspace.onDidSaveTextDocument((doc) => { + if (same(doc.uri)) this.post({ type: 'sourceDirty', dirty: false }); + }), + ]; + } + + /** Asks the page to show its Source view (Archify | Show Source). */ + reveal(): void { + this.post({ type: 'setView', view: 'source' }); + } + + /** Handles a Source message from the page; false for any other message. */ + handle(message: { type?: string }): boolean { + if (!isSourceMessage(message)) return false; + switch (message.type) { + case 'sourceReady': + void this.postDiagnostics(); + break; + case 'sourceOpen': + this.setShown(true); + void this.sendText(true); + break; + case 'sourceClose': + this.setShown(false); + break; + case 'sourceEdit': + this.edit(message); + break; + } + return true; + } + + private setShown(shown: boolean): void { + if (this.shown === shown) return; + this.shown = shown; + this.onShown(shown); + } + + private post(message: unknown): void { + Promise.resolve(this.panel.webview.postMessage(message)).catch(() => undefined); + } + + private document(): Promise { + return Promise.resolve(vscode.workspace.openTextDocument(this.uri)); + } + + private async sendText(withDiagnostics: boolean): Promise { + const doc = await this.document(); + const text = normalizeEol(doc.getText()); + this.expected.length = 0; + this.schemaKey = undefined; + this.post({ type: 'sourceText', text, schema: this.schemaChange(text), checks: CHECKS[this.kind] }); + this.post({ type: 'sourceDirty', dirty: doc.isDirty }); + if (withDiagnostics) await this.postDiagnostics(); + } + + /** The editor schema when it differs from the one the page has (a diagram's type may change), else undefined. */ + private schemaChange(text: string): { key: string; schema: object } | null | undefined { + const next = schemasFor(this.context).forSource(this.kind, text); + const key = next?.key ?? ''; + if (key === this.schemaKey) return undefined; + this.schemaKey = key; + return next ?? null; + } + + private syncFrom(doc: vscode.TextDocument): void { + const text = normalizeEol(doc.getText()); + const index = this.expected.indexOf(text); + if (index !== -1) { + // Our own edit coming back; anything older is settled too. + this.expected.splice(0, index + 1); + return; + } + this.post({ type: 'sourceText', text, schema: this.schemaChange(text), checks: CHECKS[this.kind] }); + } + + private edit(message: Extract): void { + this.expected.push(message.text); + const schema = this.schemaChange(message.text); + if (schema !== undefined) this.post({ type: 'sourceSchema', schema }); + this.queue = this.queue.then(async () => { + try { + const doc = await this.document(); + const current = normalizeEol(doc.getText()); + if (current === message.text) return; + const edit = new vscode.WorkspaceEdit(); + // Positions come from "\n" text; VS Code turns inserted text into the document's line endings. + const at = (text: string, offset: number) => { + const { line, character } = positionOf(text, offset); + return new vscode.Position(line, character); + }; + if (current === message.base) { + for (const change of message.changes) edit.replace(this.uri, new vscode.Range(at(current, change.from), at(current, change.to)), change.insert); + } else { + // The document moved on under the page (rare): the page's text wins. + edit.replace(this.uri, new vscode.Range(at(current, 0), at(current, current.length)), message.text); + } + if (!(await vscode.workspace.applyEdit(edit))) { + log().warn(`Source edit was not applied to ${this.uri.fsPath}`); + void this.sendText(false); + } + } catch (error) { + log().error(`Source edit failed for ${this.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`); + } + }); + } + + /** Problems of the file as offsets into its "\n" text, and their count for the switch. */ + async postDiagnostics(): Promise { + const diagnostics = vscode.languages.getDiagnostics(this.uri); + let items: unknown[] = []; + if (this.shown && diagnostics.length) { + const text = normalizeEol((await this.document()).getText()); + items = diagnostics.map((d) => ({ + from: offsetOf(text, d.range.start.line, d.range.start.character), + to: offsetOf(text, d.range.end.line, d.range.end.character), + severity: d.severity === vscode.DiagnosticSeverity.Error ? 'error' : d.severity === vscode.DiagnosticSeverity.Warning ? 'warning' : 'info', + message: d.message, + source: d.source, + })); + } + const count = diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning).length; + this.post({ type: 'sourceDiagnostics', items, count }); + } + + dispose(): void { + for (const d of this.disposables.splice(0)) d.dispose(); + } +} diff --git a/src/webview/page.ts b/src/webview/page.ts index 7909374..a7f72f3 100644 --- a/src/webview/page.ts +++ b/src/webview/page.ts @@ -28,6 +28,8 @@ export interface PageOptions { decisions?: DiagramDecision[]; /** Pages inside a model get a switch to the model's Graph page; `left` names the page they are on. */ graphSwitch?: { left: 'Model' | 'Diagram'; query?: string }; + /** Pages of a file that can be edited in place: the Source view's script, and the name of the page's own view. */ + source?: { scriptUri: string; left: 'Model' | 'Diagram' | 'View' }; /** The Graph page: the model as one graph, the bundled script that draws it, and the query to start with. */ graph?: { graph: ModelGraph; query?: string; scriptUri: string }; } @@ -93,6 +95,7 @@ function bridgeScript(options: PageOptions): string { decisions: options.decisions || [], descriptions: options.descriptions || {}, graphSwitch: options.graphSwitch, + source: options.source, graph: options.graph ? { graph: options.graph.graph, query: options.graph.query } : undefined, }; return `(function () { @@ -676,35 +679,97 @@ function bridgeScript(options: PageOptions): string { if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(body, anchor.nextSibling); else document.body.appendChild(body); } - // Model | Graph (or Diagram | Graph): Export's twin in Archify's toolbar, - // or at the top of the model page. Graph opens the model's Graph page. + // What the page shows: Diagram | Graph | Source (Model | Graph | Source on + // main.archify, View | Source on registry and decision pages). It sits + // left of Export in Archify's toolbar, after 2D | 3D, which only says how + // the picture is drawn; on our own pages it sits top right. Graph opens the + // model's Graph page; Source puts the file's YAML in place of the picture. + var sourceOn = false; + var syncModeSwitch = function () {}; function renderModeSwitch() { var sw = config.graphSwitch; - if (!sw || document.getElementById('archify-mode-switch')) return; - var button = el('button', 'archify-mode-switch'); - button.id = 'archify-mode-switch'; - button.type = 'button'; - button.title = 'Open the model as one graph, with a query box'; - button.setAttribute('aria-label', sw.left + ' view; switch to the model graph'); - button.appendChild(el('span', 'archify-mode-seg on', sw.left)); - button.appendChild(el('span', 'archify-mode-seg', 'Graph')); - button.addEventListener('click', function (event) { - event.stopPropagation(); - post({ type: 'showGraph', query: sw.query }); - }); + var src = config.source; + if ((!sw && !src) || document.getElementById('archify-mode-switch')) return; + var group = el('div', 'archify-mode-switch'); + group.id = 'archify-mode-switch'; + group.setAttribute('role', 'group'); + group.setAttribute('aria-label', 'View'); + function segment(label, title, action) { + var button = el('button', 'archify-mode-seg', label); + button.type = 'button'; + button.title = title; + button.addEventListener('click', function (event) { event.stopPropagation(); action(); }); + group.appendChild(button); + return button; + } + var name = (src && src.left) || sw.left; + var left = segment(name, name === 'View' ? 'Show the page' : 'Show the ' + name.toLowerCase(), function () { setSource(false); }); + if (sw) segment('Graph', 'Open the model as one graph, with a query box', function () { post({ type: 'showGraph', query: sw.query }); }); + var source = src ? segment('Source', 'Edit the YAML of this file, checked as you type', function () { setSource(true); }) : null; + if (source) { + var badge = el('span', 'archify-source-badge'); + badge.id = 'archify-source-badge'; + badge.hidden = true; + source.appendChild(badge); + } + syncModeSwitch = function () { + left.classList.toggle('on', !sourceOn); + left.setAttribute('aria-pressed', String(!sourceOn)); + if (source) { + source.classList.toggle('on', sourceOn); + source.setAttribute('aria-pressed', String(sourceOn)); + } + }; + syncModeSwitch(); var exportWrap = document.querySelector('.toolbar .export-wrap'); var head = document.querySelector('.archify-model-head'); - if (exportWrap && exportWrap.parentNode) exportWrap.parentNode.insertBefore(button, exportWrap); - else if (head) { + if (exportWrap && exportWrap.parentNode) exportWrap.parentNode.insertBefore(group, exportWrap); + else { var row = el('div', 'archify-model-switch-row'); - row.appendChild(button); - head.insertBefore(row, head.firstChild); - } else document.body.appendChild(button); + row.appendChild(group); + if (head) head.insertBefore(row, head.firstChild); + else document.body.insertBefore(row, document.body.firstChild); + } + } + + // The Source view lives in dist/source.js, loaded the first time it is chosen. + function setSource(on) { + if (!config.source || on === sourceOn) return; + sourceOn = on; + vscode.setState(Object.assign({}, vscode.getState() || {}, { sourceView: on })); + syncModeSwitch(); + if (!on) { + if (window.__archifySource) window.__archifySource.close(); + return; + } + if (window.__archifySource) { + window.__archifySource.open(); + return; + } + var script = document.createElement('script'); + script.src = config.source.scriptUri; + script.onload = function () { if (sourceOn && window.__archifySource) window.__archifySource.open(); }; + script.onerror = function () { + sourceOn = false; + syncModeSwitch(); + post({ type: 'error', message: 'The Source view could not be loaded.' }); + }; + document.head.appendChild(script); + } + + function showProblemCount(count) { + var badge = document.getElementById('archify-source-badge'); + if (!badge) return; + badge.hidden = !count; + badge.textContent = count ? String(count) : ''; + badge.title = count ? count + (count === 1 ? ' problem' : ' problems') + ' in the source' : ''; } window.addEventListener('message', function (event) { var message = event.data || {}; if (message.type === 'notice') showNotice(message.notice); + if (message.type === 'sourceDiagnostics') showProblemCount(message.count); + if (message.type === 'setView') setSource(message.view === 'source'); }); function onReady(fn) { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); @@ -717,6 +782,11 @@ function bridgeScript(options: PageOptions): string { onReady(renderDecisionsRail); onReady(describeElements); onReady(renderModeSwitch); + if (config.source) { + // The host answers with the problem count for the Source badge. + post({ type: 'sourceReady' }); + if ((vscode.getState() || {}).sourceView) onReady(function () { setSource(true); }); + } if (config.notice) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { showNotice(config.notice); }); @@ -731,6 +801,8 @@ const MODEL_STYLE = ` .archify-model { --m-text: var(--text, var(--vscode-foreground, CanvasText)); --m-muted: var(--text-muted, var(--vscode-descriptionForeground, GrayText)); --m-panel: var(--panel, var(--vscode-editorWidget-background, Canvas)); --m-border: var(--panel-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); --m-accent: var(--frontend-stroke, var(--vscode-textLink-foreground)); max-width: 1400px; margin: 0 auto; padding: 1.25rem 2rem; color: var(--m-text); font-family: var(--vscode-font-family, system-ui, sans-serif); } .archify-model-head { padding-top: 1.5rem; } +/* Under Archify's fixed toolbar (diagram pages of main.archify), start below it. */ +.toolbar ~ .archify-model-head { padding-top: 4.25rem; } .archify-model-kicker { font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--m-accent); } .archify-model-name { margin: 0.25rem 0 0.5rem; font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; } .archify-model-description { margin: 0; max-width: 72ch; font-size: 0.9375rem; line-height: 1.55; color: var(--m-muted); white-space: pre-line; } @@ -815,15 +887,19 @@ const MODEL_STYLE = ` /* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */ const SWITCH_STYLE = ` -.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; border: 0; background: transparent; color: var(--text-muted, var(--vscode-descriptionForeground)); font: inherit; cursor: pointer; position: relative; } +.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; } .toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); } .toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); } -.archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border-radius: 0.5rem; font-weight: 550; font-size: 0.8125rem; transition: background 0.15s, color 0.15s; } -.archify-mode-seg.on { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); } -.archify-mode-switch:hover .archify-mode-seg:not(.on) { color: var(--text, var(--vscode-foreground)); } -.archify-mode-switch:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 2px; border-radius: 0.75rem; } +/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */ +#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; } +#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); } +#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); } +#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; } +.archify-source-badge { display: inline-block; min-width: 1rem; margin-left: 0.35rem; padding: 0 0.3rem; border-radius: 999px; background: var(--security-stroke, var(--vscode-editorError-foreground, #f14c4c)); color: var(--bg, #fff); font-size: 0.6875rem; font-weight: 700; line-height: 1rem; text-align: center; } +.archify-source-badge[hidden] { display: none; } .archify-model-switch-row { float: right; margin: 1.5rem 0 0 1rem; } -.archify-model-switch-row .archify-mode-switch { border: 1px solid var(--m-border); border-radius: 0.75rem; background: var(--m-panel); padding: 0.1rem 0.25rem; } +body > .archify-model-switch-row { position: relative; z-index: 2; margin: 1rem 1.25rem 0 1rem; } +.archify-model-switch-row .archify-mode-switch { border: 1px solid var(--m-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); border-radius: 0.75rem; background: var(--m-panel, var(--vscode-editorWidget-background)); padding: 0.1rem 0.25rem; } `; /* Connectors rail panel: highlight of the hovered or selected edge in the SVG. */ @@ -895,8 +971,8 @@ export function prepareDiagramHtml(html: string, options: PageOptions): string { } /** The model page for a main.archify without a diagram of its own. */ -export function modelPage(options: Pick, model: ModelPanel): string { - const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {}, graphSwitch: { left: 'Model' } }); +export function modelPage(options: Pick, model: ModelPanel): string { + const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {}, graphSwitch: { left: 'Model' }, source: options.source }); return ` @@ -935,8 +1011,8 @@ export function graphPage(options: Pick, registry: RegistryPanel): string { - const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, registry, links: {} }); +export function registryPage(options: Pick, registry: RegistryPanel): string { + const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, registry, links: {}, source: options.source }); return ` @@ -947,6 +1023,8 @@ export function registryPage(options: Pick body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); } ${MODEL_STYLE} + ${SWITCH_STYLE} + ${NOTICE_STYLE} @@ -954,8 +1032,8 @@ export function registryPage(options: Pick, decision: DecisionPanel): string { - const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, decision, links: {} }); +export function decisionPage(options: Pick, decision: DecisionPanel): string { + const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, decision, links: {}, source: options.source }); return ` @@ -966,28 +1044,34 @@ export function decisionPage(options: Pick body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); } ${MODEL_STYLE} + ${SWITCH_STYLE} + ${NOTICE_STYLE} `; } -/** A small VS Code-styled page for loading and error states. */ +/** A small VS Code-styled page for loading and error states; with `source`, the YAML can be fixed in place. */ export function messagePage( - options: Pick, + options: Pick, title: string, body: string, detail?: string, ): string { + const script = options.source + ? bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, links: {}, source: options.source }) + : `(function () { var vscode = acquireVsCodeApi(); var state = ${scriptJson(options.state)}; if (state) vscode.setState(state); })();`; return ` - +