diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..acb0bb3d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test-and-package:
+ name: Test and package plugin
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ packages/description-renderer/package-lock.json
+ packages/mcp-server/package-lock.json
+
+ - name: Install root dependencies
+ run: npm ci
+
+ - name: Install description renderer dependencies
+ run: npm ci --prefix packages/description-renderer
+
+ - name: Install MCP server dependencies
+ run: npm ci --prefix packages/mcp-server
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build VSIX artifact
+ run: npm run package
+
+ - name: Upload VSIX artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: codetour-linux-x64-${{ github.sha }}
+ path: codetour-linux-x64-*.vsix
+ if-no-files-found: error
+ retention-days: 14
diff --git a/.gitignore b/.gitignore
index c597eed6..44be4a20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ node_modules
.vscode-test/
*.vsix
.DS_Store
+.agents/
diff --git a/.tours/changes.tour b/.tours/changes.tour
new file mode 100644
index 00000000..588f5c61
--- /dev/null
+++ b/.tours/changes.tour
@@ -0,0 +1,54 @@
+{
+ "$schema": "https://aka.ms/codetour-schema",
+ "title": "Descriptions de tour cliquables dans un webview",
+ "description": "Cette branche ajoute une vue agrandie pour lire la description de l'étape CodeTour active. Depuis la barre d'actions du commentaire, l'utilisateur ouvre un panneau latéral qui rend le Markdown, conserve les liens CodeTour et protège le webview contre le HTML ou les URI dangereux.\n\nElle complète aussi les consignes du serveur MCP afin que les tours générés restent lisibles et utilisent Mermaid lorsqu'un diagramme apporte une vraie vue d'ensemble.\n\nGenerated from `feature/mermaid-tour-support` (`ba7f55f65b262af892aca94fb8cb2582c0757f9b`) to `feature/clickable-tour-description-webview` (`591effd`). Les modifications locales non commitées sont exclues.",
+ "ref": "591effd",
+ "steps": [
+ {
+ "title": "Point d'entrée dans l'interface",
+ "description": "La nouvelle commande `codetour.showStepDescription` est déclarée dans le manifeste et ajoutée à la barre d'actions du commentaire CodeTour. L'icône d'aperçu apparaît en première position, tandis que les commandes de navigation et de fin de tour conservent leurs rôles.\n\nLa commande est également disponible dans la palette et dans les menus associés au contexte de lecture.",
+ "file": "package.json",
+ "pattern": "codetour.showStepDescription"
+ },
+ {
+ "title": "Enregistrement de la commande",
+ "description": "Le player relie la contribution du manifeste à `showStepDescription`. Cette petite couture garde le manifeste déclaratif et délègue toute la logique de présentation au module du webview.",
+ "file": "src/player/commands.ts",
+ "pattern": "showStepDescription"
+ },
+ {
+ "title": "Ouverture de la description active",
+ "description": "`showStepDescription` récupère le tour et l'étape actifs, ignore proprement les cas sans description, puis passe le contenu dans le pipeline de prévisualisation existant. Les liens de fichiers, références de tours, commandes et images Mermaid bénéficient ainsi des mêmes transformations que les autres surfaces de lecture.\n\nUn seul panneau est réutilisé au fil des étapes. Son titre et son contenu sont actualisés, et les références globales sont nettoyées à sa fermeture.",
+ "file": "src/player/descriptionWebview.ts",
+ "pattern": "export async function showStepDescription"
+ },
+ {
+ "title": "Liens réellement cliquables",
+ "description": "Le webview renvoie les clics au processus de l'extension. Les commandes sont limitées à `codetour.*` et `vscode.open`, les liens HTTP, HTTPS et mailto sont ouverts à l'extérieur, et les chemins relatifs sont résolus dans le workspace.\n\nLes objets URI encodés dans les arguments de commande sont reconstruits avant l'appel à VS Code, ce qui permet aux liens enrichis produits par CodeTour de continuer à fonctionner.",
+ "file": "src/player/descriptionWebview.ts",
+ "pattern": "async function openLink"
+ },
+ {
+ "title": "Rendu Markdown centralisé et sécurisé",
+ "description": "Le nouveau module convertit le Markdown avec `markdown-it`, puis nettoie systématiquement le HTML avec `sanitize-html`. L'allowlist conserve le balisage de présentation, les liens, le code et les images nécessaires au renderer Mermaid, tout en supprimant scripts, attributs et protocoles non autorisés.\n\nLe document final ajoute une Content Security Policy avec nonce, reprend les couleurs du thème VS Code et intercepte les liens non locaux pour les transmettre à l'extension.\n\n**Diagram — Flux d'une description jusqu'à l'action utilisateur**\n```mermaid\nflowchart LR\n A[Description de l'étape] --> B[Pipeline CodeTour]\n B --> C[Markdown-it]\n C --> D[Sanitization HTML]\n D --> E[Webview sécurisé]\n E -->|clic| F[Validation du lien]\n F --> G[Commande VS Code ou navigateur]\n```",
+ "file": "src/player/markdown.ts",
+ "pattern": "export function renderMarkdownToHtml"
+ },
+ {
+ "title": "Décision d'architecture",
+ "description": "L'ADR formalise la séparation entre les surfaces natives de VS Code, qui continuent de recevoir du Markdown préparé, et les surfaces HTML possédées par l'extension, qui doivent toutes passer par le renderer et le sanitizer partagés.\n\n`markdown-it` et `sanitize-html` deviennent donc des dépendances directes: leur version et leur politique de sécurité font partie du contrat de CodeTour au lieu de dépendre d'une dépendance transitive.",
+ "file": "docs/adr/0009-centralize-markdown-rendering-and-sanitization.md"
+ },
+ {
+ "title": "Couverture du comportement et de la sécurité",
+ "description": "Les tests du webview vérifient le rendu d'un titre Markdown, la transmission des clics, la Content Security Policy, l'échappement du titre du panneau et la suppression des scripts ou liens `javascript:`.\n\nUn test séparé contrôle la contribution de menu pour garantir que le nouveau bouton ne remplace pas l'action `End Tour`.",
+ "file": "src/player/descriptionWebview.test.ts"
+ },
+ {
+ "title": "Des tours MCP plus lisibles",
+ "description": "Le second commit enrichit les instructions exposées par `create_project_tour` et `create_changes_tour`. Les descriptions doivent utiliser des paragraphes courts séparés par des lignes vides, des listes lorsque cela clarifie une collection ou une séquence, et éviter les gros blocs de texte.\n\nLes tours d'architecture, de workflow, de cycle de vie ou multi-modules sont désormais encouragés à inclure un diagramme Mermaid quand il synthétise des relations réparties sur plusieurs étapes. Le schéma d'entrée et les tests d'intégration rendent ces attentes visibles et vérifiables.",
+ "file": "packages/mcp-server/src/server.ts",
+ "pattern": "MARKDOWN_WRITING_GUIDANCE"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7946d697..f8280587 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,6 +8,6 @@
"typescript.tsc.autoDetect": "off",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
- "source.organizeImports": true
+ "source.organizeImports": "explicit"
}
}
diff --git a/.vscodeignore b/.vscodeignore
index 75620af6..5880e3bc 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -1,13 +1,18 @@
.tours/**
.vscode/**
node_modules/**
+packages/**
src/**
+scripts/**
+docs/**
.gitignore
.space
package.lock.json
tsconfig.json
webpack.config.js
+dist/test/**
+dist/extension-web.js*
**/*.vsix
.git/**
.github/**
-**/*.svg
\ No newline at end of file
+**/*.svg
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c12c216f..e5ec9d37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
## Upcoming
+- Added a bundled, workspace-confined MCP server for generating Project Tours
+ and Changes Tours from VS Code and GitHub Copilot.
+- Added secure, theme-aware Mermaid diagrams to Tour descriptions and MCP
+ validation.
+
- Automatically updating a tour file as the associated code changes
- Automatically set the "pattern" record mode when you create a new tour, and select `None` for the git ref
- Added support for opening a `*.tour` file in the VS Code notebook editor (Insiders only)
diff --git a/CONTEXT.md b/CONTEXT.md
new file mode 100644
index 00000000..6422f287
--- /dev/null
+++ b/CONTEXT.md
@@ -0,0 +1,21 @@
+# CodeTour
+
+CodeTour guides a reader through selected places in a codebase using an ordered sequence of contextual explanations.
+
+## Language
+
+**Tour**:
+An ordered walkthrough of a codebase composed of Tour Steps.
+_Avoid_: Guide, presentation
+
+**Tour Step**:
+One contextual stop in a Tour, anchored to a code location or content surface and carrying a description.
+_Avoid_: Comment, slide
+
+**Mermaid Diagram**:
+A diagram whose source belongs to a Tour description and whose rendered form remains part of that description during playback.
+_Avoid_: Screenshot, generated attachment
+
+**Playback Surface**:
+A user-visible place where a Tour or Tour Step description is presented while reading a Tour.
+_Avoid_: Component, container
diff --git a/README.md b/README.md
index 46fafad7..34dc8f6c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,17 @@
# CodeTour 🗺️
+## AI-generated tours
+
+The desktop extension includes a local MCP server for generating a Project
+Tour or a Changes Tour. VS Code and GitHub Copilot discover one server per
+workspace folder automatically. The server is bundled with the extension,
+runs over stdio, remains confined to its workspace, and does not access the
+network.
+
+Tour descriptions can include captioned Mermaid diagrams. CodeTour validates
+generated diagrams before saving them and renders them as static,
+theme-aware images during playback without writing generated image files.
+
CodeTour is a Visual Studio Code extension, which allows you to record and play back guided walkthroughs of your codebases. It's like a table of contents, that can make it easier to onboard (or re-board!) to a new project/feature area, visualize bug reports, or understand the context of a code review/PR change. A "code tour" is simply a series of interactive steps, each of which are associated with a specific directory, or file/line, and include a description of the respective code. This allows developers to clone a repo, and then immediately start **learning it**, without needing to refer to a `CONTRIBUTING.md` file and/or rely on help from others. Tours can either be checked into a repo, to enable sharing with other contributors, or [exported](#exporting-tours) to a "tour file", which allows anyone to replay the same tour, without having to clone any code to do it!
diff --git a/docs/adr/0006-distribute-and-discover-the-mcp-server-with-the-extension.md b/docs/adr/0006-distribute-and-discover-the-mcp-server-with-the-extension.md
new file mode 100644
index 00000000..c0f36852
--- /dev/null
+++ b/docs/adr/0006-distribute-and-discover-the-mcp-server-with-the-extension.md
@@ -0,0 +1,11 @@
+# Distribute and discover the MCP server with the extension
+
+The desktop extension bundles the compiled stdio MCP server and registers one
+server definition per workspace folder. This allows VS Code and GitHub Copilot
+to discover the tools without a workspace-specific MCP configuration file.
+
+Each server receives one explicit workspace root and remains confined to it.
+The web extension does not register or start the Node.js server.
+
+The MCP server definition API requires VS Code 1.101, so the extension's
+minimum VS Code version moves to 1.101.
diff --git a/docs/adr/0007-render-mermaid-diagrams-within-codetour.md b/docs/adr/0007-render-mermaid-diagrams-within-codetour.md
new file mode 100644
index 00000000..fbf13f63
--- /dev/null
+++ b/docs/adr/0007-render-mermaid-diagrams-within-codetour.md
@@ -0,0 +1,19 @@
+# Render Mermaid diagrams within CodeTour
+
+CodeTour stores Mermaid source in fenced `mermaid` blocks inside a Tour's existing Markdown descriptions and renders diagrams itself, without requiring another VS Code extension or committing generated images. The Tour Generator's MCP tools validate Mermaid before writing a generated Tour, using the same exact, locked Mermaid version and diagram rules as playback. During playback, CodeTour renders with strict security, disables interactive links and HTML, and adapts the result to the active theme: Mermaid produces a theme-aware SVG internally, which CodeTour sanitizes as defense in depth and rasterizes in memory. Native comments transport the diagram as a `data:image/png;base64` image in the final Markdown, because SVG data URIs and in-memory filesystem URIs did not display as images in native comments. A rendering failure shows a compact warning and the diagram's alternative text without exposing its source, and no generated SVG or PNG file is ever written.
+
+A Mermaid Diagram is introduced by a visible Markdown caption of the form `**Diagram — …**`, which also provides its alternative text. The first version accepts `flowchart`, `sequenceDiagram`, `stateDiagram-v2`, `classDiagram`, and `erDiagram`. A single Markdown description may contain at most three diagrams, and each Mermaid source may contain at most 20 KB. Changing the active VS Code theme invalidates the render cache, immediately refreshes the visible Tour step and Tour tree, and leaves other diagrams to be regenerated on demand. Diagrams in the same description render independently, so one failure does not hide successful diagrams. Mermaid fences are excluded from CodeTour's existing `Insert Code` transformation.
+
+Packaging accounts for the rasterizer's native code by producing one VSIX per
+supported platform. The build externalizes `@resvg/resvg-js`, stages only the
+matching native binary, and verifies the staged runtime before publication.
+
+```mermaid
+flowchart LR
+ Generator[Tour Generator] -->|proposes Mermaid source| MCP[CodeTour MCP tools]
+ MCP -->|validates with the locked Mermaid| Tour[Tour file]
+ Tour -->|reads the fenced Mermaid block| Renderer[CodeTour Mermaid renderer]
+ Renderer -->|strict, theme-aware SVG| Cache[In-memory cache]
+ Cache -->|in-memory PNG as a data URI image| Surface[Markdown description surfaces]
+ Renderer -->|rendering failure| Fallback[Warning and alternative text]
+```
diff --git a/docs/adr/0008-drop-vscode-web-support.md b/docs/adr/0008-drop-vscode-web-support.md
new file mode 100644
index 00000000..6d90d60e
--- /dev/null
+++ b/docs/adr/0008-drop-vscode-web-support.md
@@ -0,0 +1,5 @@
+# Drop VS Code Web support
+
+CodeTour targets the Node-based VS Code extension host and no longer builds or publishes a browser extension. The manifest exposes only the Node `main` entry point, the build emits the Node extension and the bundled MCP server with no Web Worker bundle, and the Web-specific desktop-integration shim and the `os-browserify`/`path-browserify` fallbacks are deleted. Packaging and CI verify that no web artifact reaches the VSIX.
+
+Desktop VS Code remains supported locally and through remote Node extension hosts such as SSH, Dev Containers, and Codespaces; `extensionKind: ["workspace"]` is unchanged. The `isWeb` property of a tour's `when` expression context is retained and now always evaluates to `false`.
diff --git a/docs/adr/0009-centralize-markdown-rendering-and-sanitization.md b/docs/adr/0009-centralize-markdown-rendering-and-sanitization.md
new file mode 100644
index 00000000..7fd3fd4a
--- /dev/null
+++ b/docs/adr/0009-centralize-markdown-rendering-and-sanitization.md
@@ -0,0 +1,19 @@
+---
+status: proposed
+---
+
+# Centralize Markdown rendering and sanitization
+
+CodeTour uses one shared description pipeline for every Playback Surface. That pipeline prepares CodeTour-specific links and references, renders Mermaid diagrams, and produces the same Markdown regardless of its destination. Native VS Code surfaces continue to receive this prepared Markdown because VS Code owns their rendering; extension-owned HTML surfaces, including the Expanded Description View for the active Tour Step, convert it through a directly declared Markdown engine and an explicit HTML sanitizer before display.
+
+The Markdown engine and sanitizer are direct application dependencies rather than incidental transitive dependencies of Mermaid or packaging tools. This keeps rendering behavior versioned and testable at the CodeTour boundary, prevents a dependency update elsewhere from silently changing descriptions, and gives every extension-owned HTML surface the same allowlist for markup, attributes, URI schemes, and images.
+
+## Considered options
+
+- Relying on a Markdown engine brought transitively by another package was rejected because its presence and version are not part of CodeTour's dependency contract.
+- Letting each webview choose and configure its own renderer was rejected because rendering and security rules would drift between Playback Surfaces.
+- Replacing native VS Code Markdown rendering with generated HTML was rejected because native comments, hovers, tooltips, and notebook cells own their rendering lifecycle and capabilities.
+
+## Consequences
+
+All extension-owned HTML rendering must go through the shared renderer and sanitizer; a Playback Surface must not instantiate its own Markdown engine. Changes to rendering or the sanitizer allowlist are cross-surface behavior changes and require tests. Native surfaces may still differ visually where VS Code intentionally controls presentation, but they consume the same prepared Markdown semantics.
diff --git a/package-lock.json b/package-lock.json
index 54c0d1c4..1b58d7b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,30 +9,33 @@
"version": "0.0.59",
"license": "MIT",
"dependencies": {
+ "@resvg/resvg-js": "2.6.2",
"@types/jexl": "^2.2.0",
"axios": "^0.21.4",
"jexl": "^2.3.0",
+ "markdown-it": "^12.3.2",
"mobx": "^5.14.2",
- "os-browserify": "0.3.0",
- "path-browserify": "1.0.1",
+ "sanitize-html": "^2.13.1",
"throttle-debounce": "^3.0.1",
"vsls": "^1.0.2532"
},
"devDependencies": {
+ "@types/markdown-it": "^14.1.2",
"@types/node": "^18.14.0",
+ "@types/sanitize-html": "^2.11.0",
"@types/throttle-debounce": "^5.0.0",
- "@types/vscode": "^1.60",
+ "@types/vscode": "1.101.0",
"@vscode/vsce": "^2.17.0",
"debug": "^4.3.4",
"eslint": "^8.34.0",
"ts-loader": "^9.4.2",
- "typescript": "^4.9.5",
+ "typescript": "4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-merge": "^5.8.0"
},
"engines": {
- "vscode": "^1.60.0"
+ "vscode": "^1.101.0"
}
},
"node_modules/@babel/runtime": {
@@ -75,12 +78,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/argparse": {
- "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
- },
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -243,6 +240,233 @@
"node": ">= 8"
}
},
+ "node_modules/@resvg/resvg-js": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
+ "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@resvg/resvg-js-android-arm-eabi": "2.6.2",
+ "@resvg/resvg-js-android-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-x64": "2.6.2",
+ "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-musl": "2.6.2",
+ "@resvg/resvg-js-linux-x64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-x64-musl": "2.6.2",
+ "@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-x64-msvc": "2.6.2"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm-eabi": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
+ "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
+ "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
+ "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-x64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
+ "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
+ "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
+ "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
+ "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
+ "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
+ "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
+ "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
+ "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-x64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
+ "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz",
@@ -280,12 +504,126 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "18.14.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz",
"integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==",
"dev": true
},
+ "node_modules/@types/sanitize-html": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz",
+ "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "htmlparser2": "^8.0.0"
+ }
+ },
+ "node_modules/@types/sanitize-html/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/@types/sanitize-html/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/@types/sanitize-html/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/@types/sanitize-html/node_modules/entities": {
+ "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"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@types/sanitize-html/node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
"node_modules/@types/throttle-debounce": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
@@ -293,10 +631,11 @@
"dev": true
},
"node_modules/@types/vscode": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.60.0.tgz",
- "integrity": "sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow==",
- "dev": true
+ "version": "1.101.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.101.0.tgz",
+ "integrity": "sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@vscode/vsce": {
"version": "2.17.0",
@@ -334,12 +673,6 @@
"keytar": "^7.7.0"
}
},
- "node_modules/@vscode/vsce/node_modules/argparse": {
- "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
- },
"node_modules/@vscode/vsce/node_modules/azure-devops-node-api": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz",
@@ -359,40 +692,6 @@
"node": ">= 6"
}
},
- "node_modules/@vscode/vsce/node_modules/entities": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
- "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
- "dev": true,
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/@vscode/vsce/node_modules/linkify-it": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
- "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
- "dev": true,
- "dependencies": {
- "uc.micro": "^1.0.1"
- }
- },
- "node_modules/@vscode/vsce/node_modules/markdown-it": {
- "version": "12.3.2",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
- "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
- "dev": true,
- "dependencies": {
- "argparse": "^2.0.1",
- "entities": "~2.1.0",
- "linkify-it": "^3.0.1",
- "mdurl": "^1.0.1",
- "uc.micro": "^1.0.5"
- },
- "bin": {
- "markdown-it": "bin/markdown-it.js"
- }
- },
"node_modules/@vscode/vsce/node_modules/url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
@@ -677,6 +976,12 @@
"node": ">=4"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
"node_modules/await-semaphore": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/await-semaphore/-/await-semaphore-0.1.3.tgz",
@@ -1075,6 +1380,15 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
+ "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/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
@@ -1112,16 +1426,16 @@
}
},
"node_modules/domelementtype": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
- "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
- "dev": true,
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
- ]
+ ],
+ "license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "4.2.0",
@@ -1346,12 +1660,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/eslint/node_modules/argparse": {
- "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
- },
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2199,6 +2507,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -2238,11 +2555,35 @@
"node": ">=10"
}
},
+ "node_modules/markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
- "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=",
- "dev": true
+ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"node_modules/merge-stream": {
"version": "2.0.0",
@@ -2356,6 +2697,24 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@@ -2475,11 +2834,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/os-browserify": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
- "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
- },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -2537,6 +2891,12 @@
"semver": "^5.1.0"
}
},
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
+ "license": "MIT"
+ },
"node_modules/parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
@@ -2552,11 +2912,6 @@
"parse5": "^6.0.1"
}
},
- "node_modules/path-browserify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
- "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
- },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2597,10 +2952,10 @@
"dev": true
},
"node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.2.2",
@@ -2626,6 +2981,34 @@
"node": ">=8"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@@ -2673,15 +3056,6 @@
"once": "^1.3.1"
}
},
- "node_modules/punycode": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
- "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@@ -2904,6 +3278,115 @@
}
]
},
+ "node_modules/sanitize-html": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz",
+ "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.2.2",
+ "escape-string-regexp": "^4.0.0",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/is-plain-object": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz",
+ "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -3049,6 +3532,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -3467,6 +3959,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3478,8 +3971,7 @@
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
- "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
- "dev": true
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/underscore": {
"version": "1.13.1",
@@ -3522,6 +4014,16 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/uri-js/node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 619cb076..9ec107f2 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"license": "MIT",
"icon": "images/icon.png",
"engines": {
- "vscode": "^1.60.0"
+ "vscode": "^1.101.0"
},
"categories": [
"Other"
@@ -35,8 +35,13 @@
"onNotebookEditor:codetour"
],
"main": "./dist/extension-node.js",
- "browser": "./dist/extension-web.js",
"contributes": {
+ "mcpServerDefinitionProviders": [
+ {
+ "id": "codetour.tour-generator",
+ "label": "CodeTour Tour Generator"
+ }
+ ],
"configuration": {
"type": "object",
"title": "CodeTour",
@@ -214,6 +219,12 @@
"category": "CodeTour",
"icon": "$(eye)"
},
+ {
+ "command": "codetour.showStepDescription",
+ "title": "Afficher la description en grand",
+ "category": "CodeTour",
+ "icon": "$(preview)"
+ },
{
"command": "codetour.startTour",
"title": "Start Tour",
@@ -344,6 +355,10 @@
"command": "codetour.showMarkers",
"when": "false"
},
+ {
+ "command": "codetour.showStepDescription",
+ "when": "false"
+ },
{
"command": "codetour.unmakeTourPrimary",
"when": "false"
@@ -354,6 +369,11 @@
}
],
"comments/commentThread/title": [
+ {
+ "command": "codetour.showStepDescription",
+ "group": "inline@0",
+ "when": "commentController == codetour"
+ },
{
"command": "codetour.previousTourStep",
"group": "inline@1",
@@ -652,19 +672,22 @@
]
},
"dependencies": {
+ "@resvg/resvg-js": "2.6.2",
"@types/jexl": "^2.2.0",
"axios": "^0.21.4",
"jexl": "^2.3.0",
+ "markdown-it": "^12.3.2",
"mobx": "^5.14.2",
- "os-browserify": "0.3.0",
- "path-browserify": "1.0.1",
+ "sanitize-html": "^2.13.1",
"throttle-debounce": "^3.0.1",
"vsls": "^1.0.2532"
},
"devDependencies": {
+ "@types/markdown-it": "^14.1.2",
"@types/node": "^18.14.0",
+ "@types/sanitize-html": "^2.11.0",
"@types/throttle-debounce": "^5.0.0",
- "@types/vscode": "^1.60",
+ "@types/vscode": "1.101.0",
"@vscode/vsce": "^2.17.0",
"debug": "^4.3.4",
"eslint": "^8.34.0",
@@ -675,13 +698,22 @@
"webpack-merge": "^5.8.0"
},
"scripts": {
- "build": "webpack --mode production",
+ "prototype:mermaid-height": "python3 -m http.server 4173 --directory src/player",
+ "build:mcp": "npm --prefix packages/mcp-server run build",
+ "build:renderer": "npm --prefix packages/description-renderer run build",
+ "build": "npm run build:renderer && npm run build:mcp && webpack --mode production && npm run prepare:resvg",
+ "prepare:resvg": "node scripts/prepare-resvg-runtime.js",
+ "publish:vsix": "node scripts/publish-vsix.js",
"vscode:prepublish": "npm run build",
"watch": "webpack --mode development --watch",
- "package": "vsce package"
+ "package": "node scripts/package-extension.js",
+ "test": "npm run test:plugin && npm run test:renderer && npm run test:mcp",
+ "test:plugin": "npm run build && tsc -p tsconfig.test.json && node scripts/run-compiled-tests.js dist/test",
+ "test:renderer": "npm --prefix packages/description-renderer test",
+ "test:mcp": "npm --prefix packages/mcp-server test"
},
"prettier": {
"arrowParens": "avoid",
"trailingComma": "none"
}
-}
\ No newline at end of file
+}
diff --git a/packages/description-renderer/LICENSE.txt b/packages/description-renderer/LICENSE.txt
new file mode 100644
index 00000000..b2f52a2b
--- /dev/null
+++ b/packages/description-renderer/LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright (c) Microsoft Corporation.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/description-renderer/README.md b/packages/description-renderer/README.md
new file mode 100644
index 00000000..5fa426b5
--- /dev/null
+++ b/packages/description-renderer/README.md
@@ -0,0 +1,119 @@
+# codetour-description-renderer
+
+Offline, strict-security Mermaid rendering for [CodeTour](https://github.com/microsoft/codetour)
+Markdown descriptions. The renderer is the shared description-to-comment-content
+seam: it accepts a complete Markdown description plus the effective VS Code theme
+and returns the final Markdown used by a native VS Code comment, with each
+`mermaid` fence replaced by an in-memory PNG image or a compact warning.
+
+Design constraints:
+
+- **Offline** — no font, image, script or config fetches; rendering works with
+ network access blocked.
+- **Strict security** — Mermaid runs with `securityLevel: "strict"` and
+ `htmlLabels: false`; the intermediate SVG is additionally sanitized (scripts,
+ event handlers, `foreignObject`, anchors, remote-resource elements and all
+ external references removed) as defense in depth.
+- **In-memory transport** — Mermaid produces a theme-aware SVG internally, which
+ is rasterized to PNG with `@resvg/resvg-js` and emitted as a
+ `data:image/png;base64` image; SVG data URIs did not display as images in
+ native comments. No generated SVG or PNG file is ever written.
+- **No generated assets** — the Mermaid source stays in the tour file; nothing
+ is committed or persisted.
+- **Reuse** — rendered diagrams are reused only in memory when their exact
+ source, effective theme, renderer version and rendering options match. A
+ theme change can call `clearMermaidRenderCache()` to discard all entries;
+ failures are never kept in the cache. The MCP server (`packages/mcp-server`)
+ will validate Mermaid with the same exact, locked Mermaid version and diagram
+ rules through this package.
+
+## Diagram rules
+
+The rules below are implemented in `src/rules.ts` and are the single source of
+truth shared by playback and (later) MCP validation. They are deliberately
+syntactic and dependency-free so both sides evaluate them identically.
+
+- **Fence**: a Mermaid fence is a block whose opening line matches
+ `^\s*```mermaid[ \t]*$` and whose closing line is the next line that is only
+ backticks (`^\s*```[ \t]*$`). A `mermaid` line nested inside another fence is
+ not a Mermaid fence.
+- **Caption**: the nearest non-blank line above the fence's opening line must
+ be a caption: `^\s*\*\*(Diagram — …)\*\*[ \t]*$`, where `…` is one or more
+ characters that do not contain `**`. Blank lines may sit between the caption
+ and the fence; any other content breaks the association. The caption stays
+ visible and becomes the image alternative text.
+- **Allowlist**: the diagram kind is the first whitespace-delimited token of
+ the first significant source line (blank lines and `%%` comment/init-directive
+ lines are skipped). Exactly `flowchart`, `sequenceDiagram`,
+ `stateDiagram-v2`, `classDiagram` and `erDiagram` are supported; aliases such
+ as `graph` or `stateDiagram` are not.
+- **Count limit**: at most 3 Mermaid fences per description, counted in
+ document order. During playback the first three fences are evaluated and each
+ fence from the fourth on fails locally with a warning, regardless of its
+ content. Validation (MCP) rejects the whole description when it contains more
+ than 3 fences.
+- **Size limit**: the UTF-8 byte length of the source (the exact text between
+ the opening and closing fence lines) must be at most 20 KB (20480 bytes).
+- **Per-fence evaluation order**: caption, then size, then kind. The first
+ violated rule decides the failure. Diagrams in one description are evaluated
+ and rendered independently, so one rejected diagram never hides its siblings.
+- **Failures**: a rejected or unrenderable diagram is replaced by a single
+ compact warning line and never by its source. `renderMermaidDiagram` also
+ refuses unsupported kinds on its own, so the low-level API is safe to call
+ directly.
+
+## API
+
+```ts
+type DescriptionTheme = "light" | "dark";
+
+renderDescription(description: string, theme: DescriptionTheme): Promise;
+makeRenderedImagesResponsive(markdown: string): string;
+renderMermaidDiagram(source: string, theme: DescriptionTheme): Promise<{ svg: string; png: Buffer }>;
+clearMermaidRenderCache(): void;
+invalidateMermaidRenderCache(): void; // alias used by theme-change callers
+sanitizeSvg(svg: string): string;
+```
+
+`renderDescription` is the shared Markdown transformation used by playback
+surfaces. Its diagram work is backed by the in-memory cache; no cache entry or
+generated image is written to the workspace or persisted in VS Code state.
+`makeRenderedImagesResponsive` can then convert Markdown images into safe HTML
+images with `width="100%"`, which lets native VS Code comments and preview
+surfaces adapt them to their available width. Markdown image titles are
+preserved as HTML `title` attributes.
+Preview callers that create a `MarkdownString` must set `supportHtml = true`
+for this HTML subset to be rendered; edit-mode comments should keep it off.
+`DESCRIPTION_RENDERER_VERSION` identifies the output contract represented by
+the cache.
+
+Shared rule surface (for playback and MCP validation):
+
+```ts
+ALLOWED_DIAGRAM_KINDS: readonly ["flowchart", "sequenceDiagram", "stateDiagram-v2", "classDiagram", "erDiagram"];
+MAX_DIAGRAMS_PER_DESCRIPTION: 3;
+MAX_DIAGRAM_SOURCE_BYTES: 20480;
+
+findDiagramFences(description: string): DiagramFence[]; // caption?, source, start, end
+matchDiagramCaption(line: string): string | undefined;
+diagramKindOf(source: string): string | undefined;
+diagramSourceByteLength(source: string): number;
+isAllowedDiagramKind(kind: string): boolean;
+isMermaidFenceInfo(info: string): boolean;
+evaluateDiagramFence(fence: { caption?: string; source: string }): DiagramFenceEvaluation;
+```
+
+## Development
+
+From the repository root:
+
+```bash
+npm ci --prefix packages/description-renderer
+npm test --prefix packages/description-renderer
+```
+
+Dependencies are exact-pinned (`mermaid` 11.12.2, `jsdom` 26.1.0,
+`@resvg/resvg-js` 2.6.2) so every consumer renders with the same locked
+versions. JSDOM has no real SVG layout: text measurement relies on bounded
+heuristics (`getBBox`, `getComputedTextLength`, `getBoundingClientRect`
+polyfills), validated against all five allowed diagram kinds.
diff --git a/packages/description-renderer/package-lock.json b/packages/description-renderer/package-lock.json
new file mode 100644
index 00000000..6807af07
--- /dev/null
+++ b/packages/description-renderer/package-lock.json
@@ -0,0 +1,1990 @@
+{
+ "name": "codetour-description-renderer",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "codetour-description-renderer",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@resvg/resvg-js": "2.6.2",
+ "jsdom": "26.1.0",
+ "mermaid": "11.12.2"
+ },
+ "devDependencies": {
+ "@types/jsdom": "27.0.0",
+ "@types/node": "24.13.3",
+ "typescript": "5.9.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
+ "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
+ "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/gast/node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
+ "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
+ "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
+ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz",
+ "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
+ "node_modules/@mermaid-js/parser": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
+ "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
+ "license": "MIT",
+ "dependencies": {
+ "langium": "3.3.1"
+ }
+ },
+ "node_modules/@resvg/resvg-js": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
+ "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@resvg/resvg-js-android-arm-eabi": "2.6.2",
+ "@resvg/resvg-js-android-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-x64": "2.6.2",
+ "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-musl": "2.6.2",
+ "@resvg/resvg-js-linux-x64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-x64-musl": "2.6.2",
+ "@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-x64-msvc": "2.6.2"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm-eabi": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
+ "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
+ "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
+ "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-x64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
+ "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
+ "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
+ "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
+ "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
+ "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
+ "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
+ "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
+ "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-x64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
+ "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz",
+ "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/jsdom": {
+ "version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz",
+ "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/chevrotain": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
+ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "11.0.3",
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/regexp-to-ast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "@chevrotain/utils": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/chevrotain-allstar": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
+ "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ },
+ "peerDependencies": {
+ "chevrotain": "^11.0.0"
+ }
+ },
+ "node_modules/chevrotain/node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cytoscape": {
+ "version": "3.34.2",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.2.tgz",
+ "integrity": "sha512-Cm2jaj1X/PBNlzV9yH8zcfGOxO7U+CJ/+mxSBVPSchLaugdp4jtlGx5qaHtPRZ6tgiZ5P+o1XoRfJA+ba6KM3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.13",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz",
+ "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.23",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
+ "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.4.14",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz",
+ "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/katex": {
+ "version": "0.16.47",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
+ "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
+ "node_modules/langium": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
+ "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
+ "license": "MIT",
+ "dependencies": {
+ "chevrotain": "~11.0.3",
+ "chevrotain-allstar": "~0.3.0",
+ "vscode-languageserver": "~9.0.1",
+ "vscode-languageserver-textdocument": "~1.0.11",
+ "vscode-uri": "~3.0.8"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/mermaid": {
+ "version": "11.12.2",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
+ "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.1.1",
+ "@iconify/utils": "^3.0.1",
+ "@mermaid-js/parser": "^0.6.3",
+ "@types/d3": "^7.4.3",
+ "cytoscape": "^3.29.3",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.13",
+ "dayjs": "^1.11.18",
+ "dompurify": "^3.2.5",
+ "katex": "^0.16.22",
+ "khroma": "^2.1.0",
+ "lodash-es": "^4.17.21",
+ "marked": "^16.2.1",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.6",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^11.1.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.27",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz",
+ "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==",
+ "license": "MIT"
+ },
+ "node_modules/package-manager-detector": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
+ "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "license": "MIT"
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+ "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+ "license": "MIT"
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-dedent": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
+ "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
+ "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz",
+ "integrity": "sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+ "license": "MIT"
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/packages/description-renderer/package.json b/packages/description-renderer/package.json
new file mode 100644
index 00000000..e6955d14
--- /dev/null
+++ b/packages/description-renderer/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "codetour-description-renderer",
+ "version": "0.1.0",
+ "description": "Offline, strict-security Mermaid rendering that turns CodeTour Markdown descriptions into native comment content",
+ "license": "MIT",
+ "author": {
+ "name": "Microsoft Corporation"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/microsoft/codetour",
+ "directory": "packages/description-renderer"
+ },
+ "main": "dist/src/index.js",
+ "types": "dist/src/index.d.ts",
+ "files": [
+ "dist/src",
+ "README.md",
+ "LICENSE.txt"
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "npm run build && node ../../scripts/run-compiled-tests.js dist/test"
+ },
+ "dependencies": {
+ "@resvg/resvg-js": "2.6.2",
+ "jsdom": "26.1.0",
+ "mermaid": "11.12.2"
+ },
+ "devDependencies": {
+ "@types/jsdom": "27.0.0",
+ "@types/node": "24.13.3",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/packages/description-renderer/src/description.ts b/packages/description-renderer/src/description.ts
new file mode 100644
index 00000000..28affd63
--- /dev/null
+++ b/packages/description-renderer/src/description.ts
@@ -0,0 +1,146 @@
+import { findDiagramFences } from "./parse";
+import {
+ DescriptionTheme,
+ renderMermaidDiagram
+} from "./render";
+import {
+ DiagramRuleReason,
+ MAX_DIAGRAMS_PER_DESCRIPTION,
+ evaluateDiagramFence
+} from "./rules";
+
+const RENDER_FAILURE_NOTICES: Record = {
+ caption:
+ "> ⚠️ The Mermaid diagram requires an immediately preceding **Diagram — …** caption.",
+ size: "> ⚠️ The Mermaid diagram source exceeds the 20 KB limit.",
+ kind:
+ "> ⚠️ Unsupported Mermaid diagram kind (supported: flowchart, sequenceDiagram, stateDiagram-v2, classDiagram, erDiagram).",
+ count:
+ "> ⚠️ A description accepts at most three Mermaid diagrams; this one was not rendered.",
+ render: "> ⚠️ The Mermaid diagram could not be rendered."
+};
+
+const MARKDOWN_IMAGE_PATTERN =
+ /!\[((?:\\.|[^\]])*)\]\(((?:\\.|[^\s)])+)(?:\s+((?:"(?:\\.|[^"])*")|'(?:\\.|[^'])*'))?\)/g;
+const MARKDOWN_ALT_ESCAPE_PATTERN = /\\([[\]])/g;
+
+function escapeAltText(caption: string): string {
+ return caption.replace(/([[\]])/g, "\\$1");
+}
+
+function escapeHtmlAttribute(value: string): string {
+ return value.replace(/[&<>"']/g, character => {
+ switch (character) {
+ case "&":
+ return "&";
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case '"':
+ return """;
+ default:
+ return "'";
+ }
+ });
+}
+
+/**
+ * VS Code's native Markdown renderer supports a safe HTML subset, including
+ * percentage image widths. Convert Markdown images to that subset so they
+ * use the available width of comments and preview surfaces.
+ */
+export function makeRenderedImagesResponsive(markdown: string): string {
+ const replaceImages = (content: string): string =>
+ content.replace(
+ MARKDOWN_IMAGE_PATTERN,
+ (
+ _,
+ escapedAltText: string,
+ escapedDestination: string,
+ quotedTitle: string | undefined
+ ) => {
+ const altText = escapedAltText.replace(
+ MARKDOWN_ALT_ESCAPE_PATTERN,
+ "$1"
+ );
+ const destination = escapedDestination.replace(/\\([()\\])/g, "$1");
+ const title = quotedTitle
+ ? quotedTitle
+ .slice(1, -1)
+ .replace(/\\([\\"'])/g, "$1")
+ : undefined;
+ return (
+ '
'
+ );
+ }
+ );
+
+ let fence: { character: string; length: number } | undefined;
+ return markdown
+ .split("\n")
+ .map(line => {
+ const marker = line.match(/^\s*([\x60]{3,}|~{3,})/);
+ if (marker) {
+ const character = marker[1][0];
+ if (!fence) {
+ fence = { character, length: marker[1].length };
+ } else if (
+ fence.character === character &&
+ marker[1].length >= fence.length
+ ) {
+ fence = undefined;
+ }
+ return line;
+ }
+
+ return fence ? line : replaceImages(line);
+ })
+ .join("\n");
+}
+
+export async function renderDescription(
+ description: string,
+ theme: DescriptionTheme
+): Promise {
+ const fences = findDiagramFences(description);
+ if (fences.length === 0) {
+ return description;
+ }
+
+ let content = description;
+ for (let index = fences.length - 1; index >= 0; index--) {
+ const fence = fences[index];
+ let replacement: string;
+
+ if (index >= MAX_DIAGRAMS_PER_DESCRIPTION) {
+ replacement = RENDER_FAILURE_NOTICES.count;
+ } else if (!fence.closed) {
+ replacement = RENDER_FAILURE_NOTICES.render;
+ } else {
+ const evaluation = evaluateDiagramFence(fence);
+ if (!evaluation.allowed) {
+ replacement = RENDER_FAILURE_NOTICES[evaluation.reason];
+ } else {
+ try {
+ const { png } = await renderMermaidDiagram(fence.source, theme);
+ replacement = `})`;
+ } catch {
+ replacement = RENDER_FAILURE_NOTICES.render;
+ }
+ }
+ }
+
+ content =
+ content.slice(0, fence.start) + replacement + content.slice(fence.end);
+ }
+
+ return content;
+}
diff --git a/packages/description-renderer/src/dom.ts b/packages/description-renderer/src/dom.ts
new file mode 100644
index 00000000..20cd866b
--- /dev/null
+++ b/packages/description-renderer/src/dom.ts
@@ -0,0 +1,518 @@
+import { JSDOM } from "jsdom";
+import {
+ DEFAULT_FONT_SIZE,
+ LINE_HEIGHT_FACTOR,
+ measureTextWidth
+} from "./measure";
+
+const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+
+const DOM_GLOBAL_KEYS = [
+ "window",
+ "self",
+ "document",
+ "navigator",
+ "location",
+ "history",
+ "getComputedStyle",
+ "DOMParser",
+ "XMLSerializer",
+ "Node",
+ "Element",
+ "HTMLElement",
+ "HTMLAnchorElement",
+ "HTMLImageElement",
+ "Image",
+ "SVGElement",
+ "MutationObserver",
+ "CSSStyleDeclaration"
+];
+
+const NON_RENDERED_ELEMENTS = new Set([
+ "defs",
+ "style",
+ "title",
+ "desc",
+ "metadata",
+ "clipPath",
+ "mask",
+ "pattern",
+ "symbol",
+ "marker",
+ "linearGradient",
+ "radialGradient",
+ "filter",
+ "script"
+]);
+
+interface Box {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+interface Matrix {
+ a: number;
+ b: number;
+ c: number;
+ d: number;
+ e: number;
+ f: number;
+}
+
+const IDENTITY: Matrix = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
+
+let environment: { window: DomWindow } | undefined;
+
+export interface DomWindow {
+ document: Document;
+ DOMParser: typeof DOMParser;
+ XMLSerializer: typeof XMLSerializer;
+ SVGElement: typeof SVGElement;
+ close(): void;
+}
+
+export function ensureDomEnvironment(): DomWindow {
+ if (environment) {
+ return environment.window;
+ }
+
+ const dom = new JSDOM("", {
+ pretendToBeVisual: false,
+ url: "https://codetour.invalid/"
+ });
+
+ installSvgMeasurement(dom.window);
+
+ environment = dom;
+
+ const globals = globalThis as unknown as Record;
+ const domGlobals = dom.window as unknown as Record;
+ for (const key of DOM_GLOBAL_KEYS) {
+ if (domGlobals[key] === undefined) {
+ continue;
+ }
+ Object.defineProperty(globals, key, {
+ value: domGlobals[key],
+ configurable: true,
+ writable: true,
+ enumerable: true
+ });
+ }
+
+ return dom.window;
+}
+
+function installSvgMeasurement(window: DomWindow) {
+ const prototype = window.SVGElement && window.SVGElement.prototype;
+ if (!prototype) {
+ return;
+ }
+
+ const svgPrototype = prototype as unknown as Record;
+
+ if (typeof svgPrototype.getBBox !== "function") {
+ svgPrototype.getBBox = function(this: Element): Box {
+ return measureElementBox(this);
+ };
+ }
+
+ if (typeof svgPrototype.getComputedTextLength !== "function") {
+ svgPrototype.getComputedTextLength = function(this: Element): number {
+ return measureTextWidth(this.textContent || "", fontSizeOf(this));
+ };
+ }
+
+ svgPrototype.getBoundingClientRect = function(this: Element) {
+ const box = measureTextBox(this);
+ return {
+ x: box.x,
+ y: box.y,
+ width: box.width,
+ height: box.height,
+ top: box.y,
+ left: box.x,
+ right: box.x + box.width,
+ bottom: box.y + box.height,
+ toJSON() {
+ return box;
+ }
+ };
+ };
+}
+
+function coordinateOf(value: string | null, fontSize: number): number | undefined {
+ if (value === null) {
+ return undefined;
+ }
+ const parsed = parseFloat(value);
+ if (isNaN(parsed)) {
+ return undefined;
+ }
+ if (value.trim().endsWith("em")) {
+ return parsed * fontSize;
+ }
+ return parsed;
+}
+
+function fontSizeOf(element: Element): number {
+ let current: Element | null = element;
+ while (current) {
+ const attribute = coordinateOf(current.getAttribute("font-size"), DEFAULT_FONT_SIZE);
+ if (attribute !== undefined) {
+ return attribute;
+ }
+ const style = current.getAttribute("style");
+ if (style) {
+ const match = style.match(/font-size:\s*([\d.]+)(px|em)?/);
+ if (match) {
+ const size = parseFloat(match[1]);
+ return match[2] === "em" ? size * DEFAULT_FONT_SIZE : size;
+ }
+ }
+ current = current.parentElement;
+ }
+ return DEFAULT_FONT_SIZE;
+}
+
+function measureTextBox(element: Element): Box {
+ const fontSize = fontSizeOf(element);
+ const x = coordinateOf(element.getAttribute("x"), fontSize) || 0;
+ const tspans = Array.from(element.children).filter(
+ child => child.localName === "tspan"
+ );
+
+ let width = 0;
+ let top = Infinity;
+ let bottom = -Infinity;
+
+ const measureLine = (text: string, y: number) => {
+ width = Math.max(width, measureTextWidth(text, fontSize));
+ top = Math.min(top, y - fontSize * 0.85);
+ bottom = Math.max(bottom, y + fontSize * 0.3);
+ };
+
+ if (tspans.length > 0) {
+ let lineY = coordinateOf(element.getAttribute("y"), fontSize) || 0;
+ for (const tspan of tspans) {
+ const explicitY = coordinateOf(tspan.getAttribute("y"), fontSize);
+ const dy = coordinateOf(tspan.getAttribute("dy"), fontSize);
+ if (explicitY !== undefined) {
+ lineY = explicitY;
+ } else if (dy !== undefined) {
+ lineY += dy;
+ } else {
+ lineY += fontSize * LINE_HEIGHT_FACTOR;
+ }
+ measureLine(tspan.textContent || "", lineY);
+ }
+ } else {
+ measureLine(
+ element.textContent || "",
+ coordinateOf(element.getAttribute("y"), fontSize) || 0
+ );
+ }
+
+ if (top === Infinity) {
+ top = -fontSize;
+ bottom = fontSize * 0.3;
+ }
+
+ return {
+ x,
+ y: top,
+ width,
+ height: Math.max(bottom - top, fontSize * LINE_HEIGHT_FACTOR)
+ };
+}
+
+function union(boxes: Box[]): Box {
+ const minX = Math.min(...boxes.map(box => box.x));
+ const minY = Math.min(...boxes.map(box => box.y));
+ const maxX = Math.max(...boxes.map(box => box.x + box.width));
+ const maxY = Math.max(...boxes.map(box => box.y + box.height));
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
+}
+
+function multiplyMatrices(first: Matrix, second: Matrix): Matrix {
+ return {
+ a: first.a * second.a + first.c * second.b,
+ b: first.b * second.a + first.d * second.b,
+ c: first.a * second.c + first.c * second.d,
+ d: first.b * second.c + first.d * second.d,
+ e: first.a * second.e + first.c * second.f + first.e,
+ f: first.b * second.e + first.d * second.f + first.f
+ };
+}
+
+function parseTransform(value: string | null): Matrix {
+ if (!value) {
+ return IDENTITY;
+ }
+
+ let result = IDENTITY;
+ const pattern = /(translate|scale|rotate|matrix)\s*\(([^)]*)\)/g;
+ let match: RegExpExecArray | null;
+ while ((match = pattern.exec(value))) {
+ const args = match[2]
+ .split(/[\s,]+/)
+ .map(Number)
+ .filter(number => !isNaN(number));
+
+ let transform: Matrix;
+ if (match[1] === "translate") {
+ transform = { a: 1, b: 0, c: 0, d: 1, e: args[0] || 0, f: args[1] || 0 };
+ } else if (match[1] === "scale") {
+ const scaleX = args[0] === undefined ? 1 : args[0];
+ const scaleY = args[1] === undefined ? scaleX : args[1];
+ transform = { a: scaleX, b: 0, c: 0, d: scaleY, e: 0, f: 0 };
+ } else if (match[1] === "rotate") {
+ const angle = ((args[0] || 0) * Math.PI) / 180;
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ const rotation = { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };
+ if (args.length > 2) {
+ transform = multiplyMatrices(
+ multiplyMatrices({ a: 1, b: 0, c: 0, d: 1, e: args[1], f: args[2] }, rotation),
+ { a: 1, b: 0, c: 0, d: 1, e: -args[1], f: -args[2] }
+ );
+ } else {
+ transform = rotation;
+ }
+ } else {
+ transform = {
+ a: args[0] || 1,
+ b: args[1] || 0,
+ c: args[2] || 0,
+ d: args[3] || 1,
+ e: args[4] || 0,
+ f: args[5] || 0
+ };
+ }
+
+ result = multiplyMatrices(result, transform);
+ }
+
+ return result;
+}
+
+function transformBox(box: Box, matrix: Matrix): Box {
+ const points = [
+ { x: box.x, y: box.y },
+ { x: box.x + box.width, y: box.y },
+ { x: box.x, y: box.y + box.height },
+ { x: box.x + box.width, y: box.y + box.height }
+ ].map(point => ({
+ x: matrix.a * point.x + matrix.c * point.y + matrix.e,
+ y: matrix.b * point.x + matrix.d * point.y + matrix.f
+ }));
+
+ return union(points.map(point => ({ ...point, width: 0, height: 0 })));
+}
+
+function measureElementBox(element: Element, inherited: Matrix = IDENTITY): Box {
+ const matrix = multiplyMatrices(
+ inherited,
+ parseTransform(element.getAttribute("transform"))
+ );
+
+ const tag = element.localName;
+
+ if (tag === "text" || tag === "tspan") {
+ return transformBox(measureTextBox(element), matrix);
+ }
+
+ if (tag === "rect") {
+ return transformBox(
+ {
+ x: coordinateOf(element.getAttribute("x"), DEFAULT_FONT_SIZE) || 0,
+ y: coordinateOf(element.getAttribute("y"), DEFAULT_FONT_SIZE) || 0,
+ width: coordinateOf(element.getAttribute("width"), DEFAULT_FONT_SIZE) || 0,
+ height: coordinateOf(element.getAttribute("height"), DEFAULT_FONT_SIZE) || 0
+ },
+ matrix
+ );
+ }
+
+ if (tag === "circle") {
+ const r = coordinateOf(element.getAttribute("r"), DEFAULT_FONT_SIZE) || 0;
+ const cx = coordinateOf(element.getAttribute("cx"), DEFAULT_FONT_SIZE) || 0;
+ const cy = coordinateOf(element.getAttribute("cy"), DEFAULT_FONT_SIZE) || 0;
+ return transformBox({ x: cx - r, y: cy - r, width: r * 2, height: r * 2 }, matrix);
+ }
+
+ if (tag === "ellipse") {
+ const rx = coordinateOf(element.getAttribute("rx"), DEFAULT_FONT_SIZE) || 0;
+ const ry = coordinateOf(element.getAttribute("ry"), DEFAULT_FONT_SIZE) || 0;
+ const cx = coordinateOf(element.getAttribute("cx"), DEFAULT_FONT_SIZE) || 0;
+ const cy = coordinateOf(element.getAttribute("cy"), DEFAULT_FONT_SIZE) || 0;
+ return transformBox({ x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2 }, matrix);
+ }
+
+ if (tag === "line") {
+ const x1 = coordinateOf(element.getAttribute("x1"), DEFAULT_FONT_SIZE) || 0;
+ const y1 = coordinateOf(element.getAttribute("y1"), DEFAULT_FONT_SIZE) || 0;
+ const x2 = coordinateOf(element.getAttribute("x2"), DEFAULT_FONT_SIZE) || 0;
+ const y2 = coordinateOf(element.getAttribute("y2"), DEFAULT_FONT_SIZE) || 0;
+ return transformBox(
+ union([
+ { x: x1, y: y1, width: 0, height: 0 },
+ { x: x2, y: y2, width: 0, height: 0 }
+ ]),
+ matrix
+ );
+ }
+
+ if (tag === "path") {
+ return pathBox(element, matrix);
+ }
+
+ const boxes = Array.from(element.children)
+ .filter(child => !NON_RENDERED_ELEMENTS.has(child.localName))
+ .map(child => measureElementBox(child, matrix));
+ if (boxes.length === 0) {
+ return transformBox({ x: 0, y: 0, width: 0, height: 0 }, matrix);
+ }
+ return union(boxes);
+}
+
+function pathBox(element: Element, matrix: Matrix): Box {
+ const commands = element.getAttribute("d") || "";
+ const pattern = /([MLCQTAZmlcqtaz])([^MLCQTAZmlcqtaz]*)/g;
+ let currentX = 0;
+ let currentY = 0;
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+
+ const track = (x: number, y: number) => {
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ };
+
+ let match: RegExpExecArray | null;
+ while ((match = pattern.exec(commands))) {
+ const command = match[1];
+ const args = match[2]
+ .trim()
+ .split(/[\s,]+/)
+ .filter(part => part !== "")
+ .map(Number)
+ .filter(number => !isNaN(number));
+
+ if (command === "M" || command === "L" || command === "T") {
+ for (let index = 0; index + 1 < args.length; index += 2) {
+ currentX = args[index];
+ currentY = args[index + 1];
+ track(currentX, currentY);
+ }
+ } else if (command === "m" || command === "l" || command === "t") {
+ for (let index = 0; index + 1 < args.length; index += 2) {
+ currentX += args[index];
+ currentY += args[index + 1];
+ track(currentX, currentY);
+ }
+ } else if (command === "C") {
+ for (let index = 0; index + 5 < args.length; index += 6) {
+ track(args[index], args[index + 1]);
+ track(args[index + 2], args[index + 3]);
+ currentX = args[index + 4];
+ currentY = args[index + 5];
+ track(currentX, currentY);
+ }
+ } else if (command === "c") {
+ for (let index = 0; index + 5 < args.length; index += 6) {
+ track(currentX + args[index], currentY + args[index + 1]);
+ track(currentX + args[index + 2], currentY + args[index + 3]);
+ currentX += args[index + 4];
+ currentY += args[index + 5];
+ track(currentX, currentY);
+ }
+ } else if (command === "Q" || command === "S") {
+ for (let index = 0; index + 3 < args.length; index += 4) {
+ track(args[index], args[index + 1]);
+ currentX = args[index + 2];
+ currentY = args[index + 3];
+ track(currentX, currentY);
+ }
+ } else if (command === "q" || command === "s") {
+ for (let index = 0; index + 3 < args.length; index += 4) {
+ track(currentX + args[index], currentY + args[index + 1]);
+ currentX += args[index + 2];
+ currentY += args[index + 3];
+ track(currentX, currentY);
+ }
+ } else if (command === "H") {
+ for (const value of args) {
+ currentX = value;
+ track(currentX, currentY);
+ }
+ } else if (command === "h") {
+ for (const value of args) {
+ currentX += value;
+ track(currentX, currentY);
+ }
+ } else if (command === "V") {
+ for (const value of args) {
+ currentY = value;
+ track(currentX, currentY);
+ }
+ } else if (command === "v") {
+ for (const value of args) {
+ currentY += value;
+ track(currentX, currentY);
+ }
+ } else if (command === "A") {
+ for (let index = 5; index < args.length; index += 7) {
+ currentX = args[index];
+ currentY = args[index + 1];
+ track(currentX, currentY);
+ }
+ } else if (command === "a") {
+ for (let index = 5; index < args.length; index += 7) {
+ currentX += args[index];
+ currentY += args[index + 1];
+ track(currentX, currentY);
+ }
+ }
+ }
+
+ if (minX === Infinity) {
+ return transformBox({ x: 0, y: 0, width: 0, height: 0 }, matrix);
+ }
+
+ return transformBox(
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
+ matrix
+ );
+}
+
+export function measureContentBounds(root: Element): Box {
+ const boxes = Array.from(root.children)
+ .filter(child => !NON_RENDERED_ELEMENTS.has(child.localName))
+ .map(child => measureElementBox(child));
+ if (boxes.length === 0) {
+ return { x: 0, y: 0, width: 0, height: 0 };
+ }
+ return union(boxes);
+}
+
+export function parseSvgDocument(svg: string): Document {
+ const window = ensureDomEnvironment();
+ const document = new window.DOMParser().parseFromString(svg, "image/svg+xml");
+ if (document.documentElement.localName !== "svg") {
+ throw new Error("The rendered diagram is not a valid SVG document");
+ }
+ return document;
+}
+
+export function serializeSvgDocument(document: Document): string {
+ const window = ensureDomEnvironment();
+ return new window.XMLSerializer().serializeToString(document.documentElement);
+}
+
+export { SVG_NAMESPACE };
diff --git a/packages/description-renderer/src/index.ts b/packages/description-renderer/src/index.ts
new file mode 100644
index 00000000..6b66f3ac
--- /dev/null
+++ b/packages/description-renderer/src/index.ts
@@ -0,0 +1,31 @@
+export {
+ makeRenderedImagesResponsive,
+ renderDescription
+} from "./description";
+export {
+ clearMermaidRenderCache,
+ DESCRIPTION_RENDERER_VERSION,
+ invalidateMermaidRenderCache,
+ renderMermaidDiagram
+} from "./render";
+export type { DescriptionTheme, RenderedDiagram } from "./render";
+export { sanitizeSvg } from "./sanitize";
+export { findDiagramFences } from "./parse";
+export type { DiagramFence } from "./parse";
+export {
+ ALLOWED_DIAGRAM_KINDS,
+ MAX_DIAGRAMS_PER_DESCRIPTION,
+ MAX_DIAGRAM_SOURCE_BYTES,
+ diagramKindOf,
+ diagramSourceByteLength,
+ evaluateDiagramFence,
+ isAllowedDiagramKind,
+ isMermaidFenceInfo,
+ matchDiagramCaption
+} from "./rules";
+export type {
+ AllowedDiagramKind,
+ DiagramFenceEvaluation,
+ DiagramFenceInput,
+ DiagramRuleReason
+} from "./rules";
diff --git a/packages/description-renderer/src/measure.ts b/packages/description-renderer/src/measure.ts
new file mode 100644
index 00000000..3174fa97
--- /dev/null
+++ b/packages/description-renderer/src/measure.ts
@@ -0,0 +1,66 @@
+const NARROW_CHARACTERS = new Set([
+ "i",
+ "I",
+ "l",
+ "j",
+ "t",
+ "f",
+ ".",
+ ",",
+ ":",
+ ";",
+ "'",
+ "`",
+ "|",
+ "!",
+ "(",
+ ")",
+ "[",
+ "]",
+ "{",
+ "}",
+ "/",
+ "\\",
+ "-"
+]);
+
+const WIDE_CHARACTERS = new Set(["m", "M", "w", "W", "@", "%", "&"]);
+
+export const DEFAULT_FONT_SIZE = 16;
+export const LINE_HEIGHT_FACTOR = 1.25;
+
+function characterWidthFactor(character: string): number {
+ if (character === " ") {
+ return 0.3;
+ }
+ if (character === "\t") {
+ return 1.2;
+ }
+ if (NARROW_CHARACTERS.has(character)) {
+ return 0.32;
+ }
+ if (WIDE_CHARACTERS.has(character)) {
+ return 0.95;
+ }
+ if (character >= "A" && character <= "Z") {
+ return 0.72;
+ }
+ if (character >= "0" && character <= "9") {
+ return 0.58;
+ }
+ if (character === "—" || character === "–") {
+ return 1;
+ }
+ if (character.charCodeAt(0) > 0x2000) {
+ return 1;
+ }
+ return 0.55;
+}
+
+export function measureTextWidth(text: string, fontSize: number): number {
+ let width = 0;
+ for (const character of text) {
+ width += characterWidthFactor(character);
+ }
+ return width * fontSize;
+}
diff --git a/packages/description-renderer/src/parse.ts b/packages/description-renderer/src/parse.ts
new file mode 100644
index 00000000..ce613641
--- /dev/null
+++ b/packages/description-renderer/src/parse.ts
@@ -0,0 +1,81 @@
+import { isMermaidFenceInfo, matchDiagramCaption } from "./rules";
+
+const FENCE_INFO_PATTERN = /^\s*```(.*)$/;
+const FENCE_CLOSE_PATTERN = /^\s*```[ \t]*$/;
+const FENCE_OPEN_PATTERN = /^\s*```/;
+
+export interface DiagramFence {
+ caption?: string;
+ source: string;
+ closed: boolean;
+ start: number;
+ end: number;
+}
+
+function findCaption(lines: string[], fenceIndex: number): string | undefined {
+ for (let index = fenceIndex - 1; index >= 0; index--) {
+ const line = lines[index];
+ if (line.trim() === "") {
+ continue;
+ }
+ return matchDiagramCaption(line);
+ }
+ return undefined;
+}
+
+export function findDiagramFences(description: string): DiagramFence[] {
+ const lines = description.split("\n");
+ const lineOffsets: number[] = [];
+ let offset = 0;
+ for (const line of lines) {
+ lineOffsets.push(offset);
+ offset += line.length + 1;
+ }
+
+ const fences: DiagramFence[] = [];
+
+ let index = 0;
+ while (index < lines.length) {
+ if (!FENCE_OPEN_PATTERN.test(lines[index])) {
+ index++;
+ continue;
+ }
+
+ let closeIndex = -1;
+ for (let candidate = index + 1; candidate < lines.length; candidate++) {
+ if (FENCE_CLOSE_PATTERN.test(lines[candidate])) {
+ closeIndex = candidate;
+ break;
+ }
+ }
+
+ if (closeIndex === -1) {
+ const infoMatch = lines[index].match(FENCE_INFO_PATTERN);
+ if (infoMatch && isMermaidFenceInfo(infoMatch[1])) {
+ fences.push({
+ caption: findCaption(lines, index),
+ source: lines.slice(index + 1).join("\n"),
+ closed: false,
+ start: lineOffsets[index],
+ end: description.length
+ });
+ }
+ break;
+ }
+
+ const infoMatch = lines[index].match(FENCE_INFO_PATTERN);
+ if (infoMatch && isMermaidFenceInfo(infoMatch[1])) {
+ fences.push({
+ caption: findCaption(lines, index),
+ source: lines.slice(index + 1, closeIndex).join("\n"),
+ closed: true,
+ start: lineOffsets[index],
+ end: lineOffsets[closeIndex] + lines[closeIndex].length
+ });
+ }
+
+ index = closeIndex + 1;
+ }
+
+ return fences;
+}
diff --git a/packages/description-renderer/src/rasterize.ts b/packages/description-renderer/src/rasterize.ts
new file mode 100644
index 00000000..a5b0a7f4
--- /dev/null
+++ b/packages/description-renderer/src/rasterize.ts
@@ -0,0 +1,21 @@
+import { Resvg } from "@resvg/resvg-js";
+
+const MAX_RASTERIZED_WIDTH = 2000;
+
+export function rasterizeSvg(
+ svg: string,
+ naturalWidth: number,
+ maxRasterizedWidth: number = MAX_RASTERIZED_WIDTH
+): Buffer {
+ const resvg = new Resvg(svg, {
+ font: {
+ loadSystemFonts: true
+ },
+ fitTo:
+ naturalWidth > maxRasterizedWidth
+ ? { mode: "width", value: maxRasterizedWidth }
+ : { mode: "original" }
+ });
+
+ return resvg.render().asPng();
+}
diff --git a/packages/description-renderer/src/render.ts b/packages/description-renderer/src/render.ts
new file mode 100644
index 00000000..b9b89616
--- /dev/null
+++ b/packages/description-renderer/src/render.ts
@@ -0,0 +1,218 @@
+import { ensureDomEnvironment, measureContentBounds } from "./dom";
+import { rasterizeSvg } from "./rasterize";
+import { sanitizeSvg } from "./sanitize";
+import { diagramKindOf, isAllowedDiagramKind } from "./rules";
+
+export type DescriptionTheme = "light" | "dark";
+
+export interface RenderedDiagram {
+ svg: string;
+ png: Buffer;
+}
+
+/**
+ * Bump this when the renderer's output contract changes. The value is part of
+ * the in-memory cache key so a long-lived extension host never reuses output
+ * produced by an older renderer implementation.
+ */
+export const DESCRIPTION_RENDERER_VERSION = "0.1.0";
+
+interface MermaidRenderResult {
+ svg: string;
+}
+
+interface MermaidApi {
+ initialize(config: unknown): void;
+ parse(text: string): Promise;
+ render(id: string, text: string): Promise;
+}
+
+const MERMAID_THEMES: Record = {
+ light: "default",
+ dark: "dark"
+};
+
+const MERMAID_RENDER_OPTIONS = Object.freeze({
+ securityLevel: "strict",
+ htmlLabels: false,
+ flowchartHtmlLabels: false,
+ rasterizer: "@resvg/resvg-js@2.6.2",
+ maxRasterizedWidth: 2000,
+ viewBoxPadding: 2
+} as const);
+
+let mermaid: MermaidApi | undefined;
+let mermaidLoad: Promise | undefined;
+let diagramCounter = 0;
+const renderedDiagramCache = new Map>();
+let renderQueue = Promise.resolve();
+
+async function loadMermaid(): Promise {
+ const imported = await import("mermaid");
+ return imported.default as unknown as MermaidApi;
+}
+
+async function getMermaid(): Promise {
+ if (mermaid) {
+ return mermaid;
+ }
+
+ const load = (mermaidLoad ??= loadMermaid());
+ try {
+ mermaid = await load;
+ return mermaid;
+ } catch (error) {
+ if (mermaidLoad === load) {
+ mermaidLoad = undefined;
+ }
+ throw error;
+ }
+}
+
+function mermaidConfiguration(theme: DescriptionTheme) {
+ return {
+ startOnLoad: false,
+ securityLevel: MERMAID_RENDER_OPTIONS.securityLevel as "strict",
+ theme: MERMAID_THEMES[theme],
+ htmlLabels: MERMAID_RENDER_OPTIONS.htmlLabels,
+ flowchart: {
+ htmlLabels: MERMAID_RENDER_OPTIONS.flowchartHtmlLabels
+ }
+ };
+}
+
+function normalizeSvgSize(
+ svg: string,
+ viewBoxPadding: number
+): { svg: string; width: number } {
+ const window = ensureDomEnvironment();
+ const document = new window.DOMParser().parseFromString(svg, "image/svg+xml");
+ const root = document.documentElement;
+
+ const viewBox = (root.getAttribute("viewBox") || "")
+ .split(/[\s,]+/)
+ .map(part => Number(part))
+ .filter(part => !isNaN(part));
+
+ let bounds = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+
+ if (viewBox.length === 4) {
+ bounds = { x: viewBox[0], y: viewBox[1], width: viewBox[2], height: viewBox[3] };
+ } else {
+ bounds.width = Number(root.getAttribute("width")) || 800;
+ bounds.height = Number(root.getAttribute("height")) || 600;
+ }
+
+ const content = measureContentBounds(root);
+ bounds = {
+ x: Math.min(bounds.x, content.x - viewBoxPadding),
+ y: Math.min(bounds.y, content.y - viewBoxPadding),
+ width: 0,
+ height: 0
+ };
+ bounds.width = Math.max(
+ viewBox.length === 4 ? viewBox[0] + viewBox[2] : bounds.width,
+ content.x + content.width + viewBoxPadding
+ ) - bounds.x;
+ bounds.height = Math.max(
+ viewBox.length === 4 ? viewBox[1] + viewBox[3] : bounds.height,
+ content.y + content.height + viewBoxPadding
+ ) - bounds.y;
+
+ root.setAttribute(
+ "viewBox",
+ `${bounds.x} ${bounds.y} ${Math.ceil(bounds.width)} ${Math.ceil(bounds.height)}`
+ );
+ root.setAttribute("width", String(Math.ceil(bounds.width)));
+ root.setAttribute("height", String(Math.ceil(bounds.height)));
+ root.removeAttribute("style");
+
+ const normalized = new window.XMLSerializer().serializeToString(root);
+ return { svg: normalized, width: bounds.width };
+}
+
+async function renderMermaidDiagramUncached(
+ source: string,
+ theme: DescriptionTheme
+): Promise {
+ const render = async () => {
+ const kind = diagramKindOf(source);
+ if (!kind || !isAllowedDiagramKind(kind)) {
+ throw new Error(
+ `Unsupported Mermaid diagram kind: ${kind ?? "(none detected)"}`
+ );
+ }
+
+ ensureDomEnvironment();
+
+ const renderer = await getMermaid();
+ renderer.initialize(mermaidConfiguration(theme));
+ await renderer.parse(source);
+ const { svg } = await renderer.render(
+ `codetour-diagram-${++diagramCounter}`,
+ source
+ );
+
+ const sanitized = sanitizeSvg(svg);
+ const normalized = normalizeSvgSize(
+ sanitized,
+ MERMAID_RENDER_OPTIONS.viewBoxPadding
+ );
+ const png = rasterizeSvg(
+ normalized.svg,
+ normalized.width,
+ MERMAID_RENDER_OPTIONS.maxRasterizedWidth
+ );
+
+ return { svg: normalized.svg, png };
+ };
+
+ const queued = renderQueue.then(render, render);
+ renderQueue = queued.then(
+ () => undefined,
+ () => undefined
+ );
+ return queued;
+}
+
+function renderCacheKey(source: string, theme: DescriptionTheme): string {
+ return JSON.stringify({
+ source,
+ theme,
+ rendererVersion: DESCRIPTION_RENDERER_VERSION,
+ options: MERMAID_RENDER_OPTIONS
+ });
+}
+
+/** Clear all in-memory output, normally after VS Code changes its theme. */
+export function clearMermaidRenderCache(): void {
+ renderedDiagramCache.clear();
+}
+
+export const invalidateMermaidRenderCache = clearMermaidRenderCache;
+
+export function renderMermaidDiagram(
+ source: string,
+ theme: DescriptionTheme
+): Promise {
+ const key = renderCacheKey(source, theme);
+ const cached = renderedDiagramCache.get(key);
+ if (cached) {
+ return cached;
+ }
+
+ const rendered = renderMermaidDiagramUncached(source, theme);
+ renderedDiagramCache.set(key, rendered);
+ void rendered.catch(() => {
+ if (renderedDiagramCache.get(key) === rendered) {
+ renderedDiagramCache.delete(key);
+ }
+ });
+
+ return rendered;
+}
diff --git a/packages/description-renderer/src/rules.ts b/packages/description-renderer/src/rules.ts
new file mode 100644
index 00000000..00e0dccb
--- /dev/null
+++ b/packages/description-renderer/src/rules.ts
@@ -0,0 +1,76 @@
+export const ALLOWED_DIAGRAM_KINDS = [
+ "flowchart",
+ "sequenceDiagram",
+ "stateDiagram-v2",
+ "classDiagram",
+ "erDiagram"
+] as const;
+
+export type AllowedDiagramKind = (typeof ALLOWED_DIAGRAM_KINDS)[number];
+
+export const MAX_DIAGRAMS_PER_DESCRIPTION = 3;
+
+export const MAX_DIAGRAM_SOURCE_BYTES = 20 * 1024;
+
+export type DiagramRuleReason = "caption" | "size" | "kind";
+
+const MERMAID_FENCE_INFO_PATTERN = /^mermaid[ \t]*$/;
+
+const CAPTION_PATTERN = /^\s*\*\*(Diagram — (?:(?!\*\*).)+)\*\*[ \t]*$/;
+
+const SIGNIFICANT_LINE_PATTERN = /^(?!\s*%%)[^\s]/;
+
+export function isMermaidFenceInfo(info: string): boolean {
+ return MERMAID_FENCE_INFO_PATTERN.test(info);
+}
+
+export function isAllowedDiagramKind(kind: string): kind is AllowedDiagramKind {
+ return (ALLOWED_DIAGRAM_KINDS as readonly string[]).includes(kind);
+}
+
+export function matchDiagramCaption(line: string): string | undefined {
+ const match = line.match(CAPTION_PATTERN);
+ return match ? match[1].trim() : undefined;
+}
+
+export function diagramKindOf(source: string): string | undefined {
+ for (const line of source.split("\n")) {
+ if (!SIGNIFICANT_LINE_PATTERN.test(line)) {
+ continue;
+ }
+ return line.trim().split(/\s+/)[0];
+ }
+ return undefined;
+}
+
+export function diagramSourceByteLength(source: string): number {
+ return Buffer.byteLength(source, "utf8");
+}
+
+export interface DiagramFenceInput {
+ caption?: string;
+ source: string;
+}
+
+export type DiagramFenceEvaluation =
+ | { allowed: true; kind: AllowedDiagramKind }
+ | { allowed: false; reason: DiagramRuleReason; kind?: string };
+
+export function evaluateDiagramFence(
+ fence: DiagramFenceInput
+): DiagramFenceEvaluation {
+ if (!fence.caption) {
+ return { allowed: false, reason: "caption" };
+ }
+
+ if (diagramSourceByteLength(fence.source) > MAX_DIAGRAM_SOURCE_BYTES) {
+ return { allowed: false, reason: "size" };
+ }
+
+ const kind = diagramKindOf(fence.source);
+ if (!kind || !isAllowedDiagramKind(kind)) {
+ return { allowed: false, reason: "kind", kind };
+ }
+
+ return { allowed: true, kind };
+}
diff --git a/packages/description-renderer/src/sanitize.ts b/packages/description-renderer/src/sanitize.ts
new file mode 100644
index 00000000..fb27702f
--- /dev/null
+++ b/packages/description-renderer/src/sanitize.ts
@@ -0,0 +1,55 @@
+import { parseSvgDocument, serializeSvgDocument } from "./dom";
+
+const BLOCKED_ELEMENTS = new Set([
+ "script",
+ "foreignObject",
+ "iframe",
+ "object",
+ "embed",
+ "use",
+ "link",
+ "meta",
+ "base",
+ "image",
+ "img"
+]);
+
+const BLOCKED_ATTRIBUTE_PATTERN = /^on/i;
+const EXTERNAL_REFERENCE_PATTERN = /^(?!#)/;
+
+function sanitizeElement(element: Element): void {
+ for (const child of Array.from(element.children)) {
+ if (child.localName === "a") {
+ sanitizeElement(child);
+ for (const grandChild of Array.from(child.children)) {
+ element.insertBefore(grandChild, child);
+ }
+ element.removeChild(child);
+ } else if (BLOCKED_ELEMENTS.has(child.localName)) {
+ element.removeChild(child);
+ } else {
+ sanitizeElement(child);
+ }
+ }
+
+ for (const attribute of Array.from(element.attributes)) {
+ const name = attribute.name;
+ if (BLOCKED_ATTRIBUTE_PATTERN.test(name)) {
+ element.removeAttribute(name);
+ continue;
+ }
+
+ if (
+ (name === "href" || name === "xlink:href") &&
+ EXTERNAL_REFERENCE_PATTERN.test(attribute.value.trim())
+ ) {
+ element.removeAttribute(name);
+ }
+ }
+}
+
+export function sanitizeSvg(svg: string): string {
+ const document = parseSvgDocument(svg);
+ sanitizeElement(document.documentElement);
+ return serializeSvgDocument(document);
+}
diff --git a/packages/description-renderer/test/cache.test.ts b/packages/description-renderer/test/cache.test.ts
new file mode 100644
index 00000000..3f941d25
--- /dev/null
+++ b/packages/description-renderer/test/cache.test.ts
@@ -0,0 +1,41 @@
+import { test } from "node:test";
+import * as assert from "node:assert";
+import {
+ clearMermaidRenderCache,
+ invalidateMermaidRenderCache,
+ renderMermaidDiagram
+} from "../src/render";
+import { FLOWCHART_SOURCE } from "./helpers/fixtures";
+
+test.beforeEach(() => {
+ clearMermaidRenderCache();
+});
+
+test("renderMermaidDiagram reuses the exact source and effective theme", async () => {
+ const first = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+ const second = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+
+ assert.strictEqual(second, first);
+});
+
+test("renderMermaidDiagram keeps exact source and theme variants separate", async () => {
+ const light = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+ const dark = await renderMermaidDiagram(FLOWCHART_SOURCE, "dark");
+ const sourceVariant = await renderMermaidDiagram(
+ `${FLOWCHART_SOURCE}\n`,
+ "light"
+ );
+
+ assert.notStrictEqual(dark, light);
+ assert.notStrictEqual(sourceVariant, light);
+ assert.notEqual(dark.svg, light.svg);
+});
+
+test("theme invalidation drops cached rendered diagrams", async () => {
+ const first = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+
+ invalidateMermaidRenderCache();
+
+ const second = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+ assert.notStrictEqual(second, first);
+});
diff --git a/packages/description-renderer/test/helpers/fixtures.ts b/packages/description-renderer/test/helpers/fixtures.ts
new file mode 100644
index 00000000..83a4ff3e
--- /dev/null
+++ b/packages/description-renderer/test/helpers/fixtures.ts
@@ -0,0 +1,213 @@
+export const PNG_SIGNATURE = Buffer.from([
+ 0x89,
+ 0x50,
+ 0x4e,
+ 0x47,
+ 0x0d,
+ 0x0a,
+ 0x1a,
+ 0x0a
+]);
+
+export function escapeRegExp(text: string): string {
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+export function extractPngDataUri(markdown: string, altText: string): Buffer {
+ const pattern = new RegExp(
+ `!\\[${escapeRegExp(altText)}\\]\\(data:image\\/png;base64,([A-Za-z0-9+/=]+)\\)`
+ );
+ const match = markdown.match(pattern);
+ if (!match) {
+ throw new Error(
+ `Expected a PNG data URI image with alt text "${altText}" in:\n${markdown.slice(0, 400)}`
+ );
+ }
+ return Buffer.from(match[1], "base64");
+}
+
+export function assertValidPng(png: Buffer): void {
+ if (png.length < 200) {
+ throw new Error(`Expected a non-trivial PNG, got ${png.length} bytes`);
+ }
+ if (!png.subarray(0, 8).equals(PNG_SIGNATURE)) {
+ throw new Error("Expected a PNG signature at the start of the image");
+ }
+ const width = png.readUInt32BE(16);
+ const height = png.readUInt32BE(20);
+ if (width < 20 || height < 20) {
+ throw new Error(`Expected a visible diagram size, got ${width}x${height}`);
+ }
+ return void [width, height];
+}
+
+export function captionedDiagram(caption: string, source: string): string {
+ return ["**" + caption + "**", "", "```mermaid", source, "```"].join("\n");
+}
+
+export function findImageLine(markdown: string, altText: string): string {
+ const prefix = escapeRegExp(altText).replace(
+ /(\\\[|\\\])/g,
+ "\\\\?$1"
+ );
+ const pattern = new RegExp(`^!\\[${prefix}`);
+ const line = markdown.split("\n").find(line => pattern.test(line));
+ if (!line) {
+ throw new Error(
+ `Expected an image line with alt text "${altText}" in:\n${markdown.slice(0, 400)}`
+ );
+ }
+ return line;
+}
+
+export const FLOWCHART_SOURCE = [
+ "flowchart TD",
+ " Client --> Gateway --> Service",
+ " Service --> Database"
+].join("\n");
+
+export const CAPTIONED_FLOWCHART_DESCRIPTION = [
+ "The request flows through three layers:",
+ "",
+ "**Diagram — Request lifecycle**",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```",
+ "",
+ "That is the whole story."
+].join("\n");
+
+export const ALLOWED_KIND_SOURCES: Record = {
+ flowchart: FLOWCHART_SOURCE,
+ sequenceDiagram: [
+ "sequenceDiagram",
+ " autonumber",
+ " participant User",
+ " participant Player as Tour Player",
+ " User->>Player: Start tour",
+ " Player-->>User: Step ready",
+ " note right of Player: Diagrams render per fence"
+ ].join("\n"),
+ "stateDiagram-v2": [
+ "stateDiagram-v2",
+ " [*] --> Idle",
+ " Idle --> Loading: start tour",
+ " Loading --> Playing: step found",
+ " note right of Loading : waiting for the file",
+ " Playing --> Done: finish",
+ " Done --> [*]"
+ ].join("\n"),
+ classDiagram: [
+ "classDiagram",
+ " class CodeTour {",
+ " +string title",
+ " +Step[] steps",
+ " +start() void",
+ " -persist()",
+ " }",
+ " class Step {",
+ " +string description",
+ " }",
+ " CodeTour \"1\" *-- \"many\" Step : contains"
+ ].join("\n"),
+ erDiagram: [
+ "erDiagram",
+ " TOUR ||--o{ STEP : contains",
+ " STEP ||--|| FILE : anchors",
+ " TOUR {",
+ " string title \"the tour title\"",
+ " }",
+ " STEP {",
+ " string description",
+ " int line",
+ " }"
+ ].join("\n")
+};
+
+export const ALLOWED_KIND_CAPTIONS: Record = {
+ flowchart: "Diagram — Request lifecycle",
+ sequenceDiagram: "Diagram — Tour playback sequence",
+ "stateDiagram-v2": "Diagram — Tour playback states",
+ classDiagram: "Diagram — Tour data model",
+ erDiagram: "Diagram — Tour entity relationships"
+};
+
+export const UNSUPPORTED_KIND_SOURCES: Record = {
+ pie: ['pie title Pets adopted', ' "Dogs" : 386', ' "Cats" : 85'].join(
+ "\n"
+ ),
+ gantt: [
+ "gantt",
+ " title Release plan",
+ " dateFormat YYYY-MM-DD",
+ " section Build",
+ " Package :p1, 2024-01-01, 3d"
+ ].join("\n"),
+ gitGraph: ["gitGraph", " commit", " branch feature", " commit"].join(
+ "\n"
+ ),
+ mindmap: ["mindmap", " root((tour))", " steps"].join("\n"),
+ journey: [
+ "journey",
+ " title Taking a tour",
+ " section Start",
+ " Me: 5: Me"
+ ].join("\n"),
+ "unknown kind": ["squarewave TD", " A --> B"].join("\n")
+};
+
+export function flowchartOfExactByteLength(bytes: number): string {
+ const base = "flowchart TD\n A --> B\n%%";
+ const padding = bytes - Buffer.byteLength(base, "utf8");
+ if (padding < 0) {
+ throw new Error(`Cannot build a flowchart of only ${bytes} bytes`);
+ }
+ return base + "x".repeat(padding);
+}
+
+export const HOSTILE_LABEL_SOURCES: Record = {
+ flowchart: [
+ "flowchart TD",
+ ' A["
"] --> B[""]'
+ ].join("\n"),
+ sequenceDiagram: [
+ "sequenceDiagram",
+ ' participant U as "
"',
+ ' U->>U: ""'
+ ].join("\n"),
+ "stateDiagram-v2": [
+ "stateDiagram-v2",
+ " [*] --> Idle",
+ ' Idle --> Playing: "
"',
+ ' state "" as S',
+ " Playing --> S"
+ ].join("\n"),
+ classDiagram: [
+ "classDiagram",
+ ' class Foo["
"] {',
+ " +string bar",
+ " }",
+ ' note for Foo "a note with "'
+ ].join("\n"),
+ erDiagram: [
+ "erDiagram",
+ " TOUR {",
+ ' string title "comment "',
+ " }"
+ ].join("\n")
+};
+
+export const HOSTILE_INTERACTION_SOURCE = [
+ "flowchart TD",
+ " A --> B",
+ ' click A "https://example.com" "tooltip"',
+ ' click B href "https://example.com/linked" "tooltip"',
+ ' click A call alert(1) "tooltip"',
+ " linkStyle 0 stroke:#f66,stroke-width:2px"
+].join("\n");
+
+export const HOSTILE_MARKDOWN_LABEL_SOURCE = [
+ "flowchart TD",
+ ' A["See [docs](https://evil.example) and [run](command:codetour.nextTourStep)"] --> B'
+].join("\n");
diff --git a/packages/description-renderer/test/mermaid-save.test.ts b/packages/description-renderer/test/mermaid-save.test.ts
new file mode 100644
index 00000000..3aae9e35
--- /dev/null
+++ b/packages/description-renderer/test/mermaid-save.test.ts
@@ -0,0 +1,56 @@
+import { test } from "node:test";
+import * as assert from "node:assert";
+import { renderDescription } from "../src/description";
+import { clearMermaidRenderCache, renderMermaidDiagram } from "../src/render";
+import {
+ assertValidPng,
+ captionedDiagram,
+ extractPngDataUri,
+ FLOWCHART_SOURCE
+} from "./helpers/fixtures";
+
+// Regression for the "Save Step" flow not re-rendering Mermaid. After the
+// source is edited and saved, the preview must show a PNG for the new source,
+// hide the fence, and no longer match the previous diagram.
+const PRIOR_SOURCE = FLOWCHART_SOURCE; // Client --> Gateway --> Service
+const SAVED_SOURCE = [
+ "flowchart TD",
+ " Author --> Reviewer --> Merge"
+].join("\n");
+const CAPTION = "Diagram — Saved step";
+
+test.beforeEach(() => {
+ clearMermaidRenderCache();
+});
+
+test("the preview after a save renders the new source as a PNG without a fence", async () => {
+ // Before the edit, the step previewed the prior diagram.
+ const priorPreview = await renderDescription(
+ captionedDiagram(CAPTION, PRIOR_SOURCE),
+ "light"
+ );
+ assertValidPng(extractPngDataUri(priorPreview, CAPTION));
+
+ // After saving the edited source, the preview is regenerated from it.
+ const savedPreview = await renderDescription(
+ captionedDiagram(CAPTION, SAVED_SOURCE),
+ "light"
+ );
+
+ assertValidPng(extractPngDataUri(savedPreview, CAPTION));
+ assert.ok(!savedPreview.includes("```mermaid"));
+ assert.ok(!savedPreview.includes("flowchart TD"));
+ assert.ok(!savedPreview.includes("Author --> Reviewer"));
+ assert.ok(!savedPreview.includes("Client --> Gateway"));
+});
+
+test("the regenerated image matches the saved source, not the prior one", async () => {
+ const prior = await renderMermaidDiagram(PRIOR_SOURCE, "light");
+ const saved = await renderMermaidDiagram(SAVED_SOURCE, "light");
+
+ assert.ok(prior.svg.includes("Client"));
+ assert.ok(!prior.svg.includes("Author"));
+ assert.ok(saved.svg.includes("Author"));
+ assert.ok(!saved.svg.includes("Client"));
+ assert.ok(Buffer.compare(saved.png, prior.png) !== 0);
+});
diff --git a/packages/description-renderer/test/offline.test.ts b/packages/description-renderer/test/offline.test.ts
new file mode 100644
index 00000000..0f9b103b
--- /dev/null
+++ b/packages/description-renderer/test/offline.test.ts
@@ -0,0 +1,141 @@
+import { test } from "node:test";
+import * as assert from "node:assert";
+import * as fs from "node:fs";
+import * as net from "node:net";
+import * as os from "node:os";
+import * as path from "node:path";
+import { renderDescription } from "../src/description";
+import {
+ ALLOWED_KIND_CAPTIONS,
+ ALLOWED_KIND_SOURCES,
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ assertValidPng,
+ captionedDiagram,
+ extractPngDataUri
+} from "./helpers/fixtures";
+
+function blockNetwork(): { attempts: () => number; restore: () => void } {
+ const originalFetch = globalThis.fetch;
+ const originalConnect = net.Socket.prototype.connect;
+ let attempted = 0;
+
+ const countAttempt = () => {
+ attempted++;
+ };
+
+ const failingConnect = (function(
+ this: net.Socket,
+ ...args: unknown[]
+ ): net.Socket {
+ countAttempt();
+ throw new Error("Network access was attempted during rendering");
+ } as unknown as typeof net.Socket.prototype.connect);
+
+ globalThis.fetch = (async () => {
+ countAttempt();
+ throw new Error("Network access was attempted during rendering");
+ }) as typeof fetch;
+
+ Object.defineProperty(net.Socket.prototype, "connect", {
+ value: failingConnect,
+ configurable: true,
+ writable: true,
+ enumerable: false
+ });
+
+ return {
+ attempts: () => attempted,
+ restore() {
+ globalThis.fetch = originalFetch;
+ Object.defineProperty(net.Socket.prototype, "connect", {
+ value: originalConnect,
+ configurable: true,
+ writable: true,
+ enumerable: false
+ });
+ }
+ };
+}
+
+function listFilesRecursive(directory: string): string[] {
+ const entries: string[] = [];
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ entries.push(...listFilesRecursive(entryPath));
+ } else {
+ entries.push(entryPath);
+ }
+ }
+ return entries;
+}
+
+test("renderDescription renders a flowchart with network access blocked", async () => {
+ const network = blockNetwork();
+
+ try {
+ const content = await renderDescription(
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ "light"
+ );
+ assertValidPng(extractPngDataUri(content, "Diagram — Request lifecycle"));
+ assert.equal(network.attempts(), 0);
+ } finally {
+ network.restore();
+ }
+});
+
+test("renderDescription renders the other allowed kinds with network access blocked", async () => {
+ const network = blockNetwork();
+
+ try {
+ for (const kind of [
+ "sequenceDiagram",
+ "stateDiagram-v2",
+ "classDiagram",
+ "erDiagram"
+ ]) {
+ const caption = ALLOWED_KIND_CAPTIONS[kind];
+ const content = await renderDescription(
+ captionedDiagram(caption, ALLOWED_KIND_SOURCES[kind]),
+ "light"
+ );
+ assertValidPng(extractPngDataUri(content, caption));
+ }
+ assert.equal(network.attempts(), 0);
+ } finally {
+ network.restore();
+ }
+});
+
+test("renderDescription creates no generated SVG or PNG file", async () => {
+ const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "codetour-renderer-"));
+ const previousCwd = process.cwd();
+
+ try {
+ process.chdir(sandbox);
+ const description = [
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ "",
+ captionedDiagram(
+ ALLOWED_KIND_CAPTIONS["stateDiagram-v2"],
+ ALLOWED_KIND_SOURCES["stateDiagram-v2"]
+ )
+ ].join("\n");
+ const content = await renderDescription(description, "dark");
+ assertValidPng(extractPngDataUri(content, "Diagram — Request lifecycle"));
+ assertValidPng(
+ extractPngDataUri(content, ALLOWED_KIND_CAPTIONS["stateDiagram-v2"])
+ );
+
+ const files = listFilesRecursive(sandbox);
+ assert.deepEqual(
+ files,
+ [],
+ "Expected the renderer to leave the workspace without generated assets"
+ );
+ } finally {
+ process.chdir(previousCwd);
+ fs.rmSync(sandbox, { recursive: true, force: true });
+ }
+});
diff --git a/packages/description-renderer/test/rules.test.ts b/packages/description-renderer/test/rules.test.ts
new file mode 100644
index 00000000..bf66b6b6
--- /dev/null
+++ b/packages/description-renderer/test/rules.test.ts
@@ -0,0 +1,160 @@
+import { test } from "node:test";
+import * as assert from "node:assert";
+import {
+ ALLOWED_DIAGRAM_KINDS,
+ MAX_DIAGRAMS_PER_DESCRIPTION,
+ MAX_DIAGRAM_SOURCE_BYTES,
+ diagramKindOf,
+ diagramSourceByteLength,
+ evaluateDiagramFence,
+ isAllowedDiagramKind,
+ isMermaidFenceInfo,
+ matchDiagramCaption
+} from "../src/rules";
+
+test("the allowlist accepts exactly the five first-version diagram kinds", () => {
+ assert.deepEqual(ALLOWED_DIAGRAM_KINDS, [
+ "flowchart",
+ "sequenceDiagram",
+ "stateDiagram-v2",
+ "classDiagram",
+ "erDiagram"
+ ]);
+ assert.equal(MAX_DIAGRAMS_PER_DESCRIPTION, 3);
+ assert.equal(MAX_DIAGRAM_SOURCE_BYTES, 20 * 1024);
+});
+
+test("diagramKindOf reads the kind from the first significant line", () => {
+ assert.equal(diagramKindOf("flowchart TD\n A --> B"), "flowchart");
+ assert.equal(diagramKindOf("sequenceDiagram\n A->>B: hi"), "sequenceDiagram");
+ assert.equal(
+ diagramKindOf("stateDiagram-v2\n [*] --> Idle"),
+ "stateDiagram-v2"
+ );
+ assert.equal(diagramKindOf("classDiagram\n class A"), "classDiagram");
+ assert.equal(diagramKindOf("erDiagram\n A ||--o{ B : c"), "erDiagram");
+});
+
+test("diagramKindOf skips blank lines, comments and init directives", () => {
+ assert.equal(
+ diagramKindOf("\n\n%% a comment\n%%{init: {'theme':'base'}}%%\nflowchart TD\n A --> B"),
+ "flowchart"
+ );
+});
+
+test("diagramKindOf returns undefined for empty or comment-only sources", () => {
+ assert.equal(diagramKindOf(""), undefined);
+ assert.equal(diagramKindOf("%% only a comment"), undefined);
+ assert.equal(diagramKindOf(" \n \n"), undefined);
+});
+
+test("diagramKindOf takes the first token, not the whole line", () => {
+ assert.equal(diagramKindOf("pie title Pets"), "pie");
+ assert.equal(diagramKindOf("squarewave TD"), "squarewave");
+});
+
+test("the allowlist is exact: aliases and other kinds are unsupported", () => {
+ assert.equal(isAllowedDiagramKind("graph"), false);
+ assert.equal(isAllowedDiagramKind("stateDiagram"), false);
+ for (const kind of ["pie", "gantt", "mindmap", "journey", "gitGraph"]) {
+ assert.equal(isAllowedDiagramKind(kind), false);
+ }
+ assert.equal(isAllowedDiagramKind("Flowchart"), false);
+});
+
+test("isMermaidFenceInfo accepts only the bare mermaid info string", () => {
+ assert.equal(isMermaidFenceInfo("mermaid"), true);
+ assert.equal(isMermaidFenceInfo("mermaid "), true);
+ assert.equal(isMermaidFenceInfo("mermaid\t"), true);
+ assert.equal(isMermaidFenceInfo("ts"), false);
+ assert.equal(isMermaidFenceInfo("mermaid x"), false);
+ assert.equal(isMermaidFenceInfo("Mermaid"), false);
+ assert.equal(isMermaidFenceInfo(""), false);
+});
+
+test("matchDiagramCaption accepts a visible Diagram caption line", () => {
+ assert.equal(
+ matchDiagramCaption("**Diagram — Request lifecycle**"),
+ "Diagram — Request lifecycle"
+ );
+ assert.equal(
+ matchDiagramCaption(" **Diagram — Padded** "),
+ "Diagram — Padded"
+ );
+ assert.equal(
+ matchDiagramCaption("**Diagram — Multi word caption with — dashes**"),
+ "Diagram — Multi word caption with — dashes"
+ );
+});
+
+test("matchDiagramCaption rejects malformed caption lines", () => {
+ assert.equal(matchDiagramCaption("*Diagram — single stars*"), undefined);
+ assert.equal(matchDiagramCaption("**Diagram – hyphen**"), undefined);
+ assert.equal(matchDiagramCaption("**Diagram — **"), undefined);
+ assert.equal(matchDiagramCaption("**Diagram —**"), undefined);
+ assert.equal(matchDiagramCaption("Diagram — bare"), undefined);
+ assert.equal(matchDiagramCaption("**Diagram — a** and **b**"), undefined);
+ assert.equal(matchDiagramCaption("**Not a diagram — caption**"), undefined);
+ assert.equal(matchDiagramCaption("Some intro text"), undefined);
+ assert.equal(matchDiagramCaption("```"), undefined);
+ assert.equal(matchDiagramCaption(""), undefined);
+});
+
+test("diagramSourceByteLength counts UTF-8 bytes, not characters", () => {
+ assert.equal(diagramSourceByteLength("flowchart TD"), 12);
+ assert.equal(diagramSourceByteLength("ééé"), 6);
+ assert.equal(diagramSourceByteLength("—"), 3);
+});
+
+test("evaluateDiagramFence allows an in-bounds supported diagram", () => {
+ assert.deepEqual(
+ evaluateDiagramFence({ caption: "Diagram — x", source: "flowchart TD\n A --> B" }),
+ { allowed: true, kind: "flowchart" }
+ );
+});
+
+test("evaluateDiagramFence rejects a missing caption before anything else", () => {
+ const evaluation = evaluateDiagramFence({
+ source: "pie title way too large" + "x".repeat(MAX_DIAGRAM_SOURCE_BYTES)
+ });
+ assert.deepEqual(evaluation, { allowed: false, reason: "caption" });
+});
+
+test("evaluateDiagramFence rejects an oversized source before reading its kind", () => {
+ const evaluation = evaluateDiagramFence({
+ caption: "Diagram — too big",
+ source: "flowchart TD\n%%" + "x".repeat(MAX_DIAGRAM_SOURCE_BYTES)
+ });
+ assert.deepEqual(evaluation, { allowed: false, reason: "size" });
+});
+
+test("evaluateDiagramFence rejects an unsupported kind", () => {
+ const evaluation = evaluateDiagramFence({
+ caption: "Diagram — pie",
+ source: "pie title Pets\n \"Dogs\" : 386"
+ });
+ assert.deepEqual(evaluation, { allowed: false, reason: "kind", kind: "pie" });
+});
+
+test("evaluateDiagramFence reports the detected kind alongside the rejection", () => {
+ const evaluation = evaluateDiagramFence({
+ caption: "Diagram — gantt",
+ source: "gantt\n dateFormat YYYY-MM-DD"
+ });
+ assert.equal(evaluation.allowed, false);
+ if (!evaluation.allowed) {
+ assert.equal(evaluation.reason, "kind");
+ assert.equal(evaluation.kind, "gantt");
+ }
+});
+
+test("evaluateDiagramFence accepts a source of exactly 20 KB", () => {
+ const source = "flowchart TD\n%%" + "x".repeat(
+ MAX_DIAGRAM_SOURCE_BYTES - Buffer.byteLength("flowchart TD\n%%", "utf8")
+ );
+ const evaluation = evaluateDiagramFence({
+ caption: "Diagram — boundary",
+ source
+ });
+ assert.deepEqual(evaluation, { allowed: true, kind: "flowchart" });
+});
diff --git a/packages/description-renderer/test/seam.test.ts b/packages/description-renderer/test/seam.test.ts
new file mode 100644
index 00000000..cb271e18
--- /dev/null
+++ b/packages/description-renderer/test/seam.test.ts
@@ -0,0 +1,410 @@
+import { test } from "node:test";
+import * as assert from "node:assert";
+import {
+ makeRenderedImagesResponsive,
+ renderDescription
+} from "../src/description";
+import { renderMermaidDiagram } from "../src/render";
+import {
+ ALLOWED_KIND_CAPTIONS,
+ ALLOWED_KIND_SOURCES,
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ FLOWCHART_SOURCE,
+ UNSUPPORTED_KIND_SOURCES,
+ captionedDiagram,
+ extractPngDataUri,
+ flowchartOfExactByteLength,
+ assertValidPng
+} from "./helpers/fixtures";
+
+const COUNT_NOTICE_MARKER = "at most three Mermaid diagrams";
+const CAPTION_NOTICE_MARKER = "**Diagram — …** caption";
+const SIZE_NOTICE_MARKER = "20 KB limit";
+const KIND_NOTICE_MARKER = "Unsupported Mermaid diagram kind";
+const RENDER_NOTICE_MARKER = "could not be rendered";
+
+function assertNotice(content: string, marker: string): void {
+ assert.ok(
+ content.includes(marker),
+ `Expected the notice "${marker}" in:\n${content.slice(0, 400)}`
+ );
+}
+
+test("renderDescription renders a captioned flowchart as a PNG image", async () => {
+ const content = await renderDescription(
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ "light"
+ );
+
+ const png = extractPngDataUri(content, "Diagram — Request lifecycle");
+ assertValidPng(png);
+});
+
+test("renderDescription keeps the caption visible and uses it as alt text", async () => {
+ const content = await renderDescription(
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ "light"
+ );
+
+ assert.ok(content.includes("**Diagram — Request lifecycle**"));
+ assert.ok(content.includes(");
+});
+
+test("renderDescription hides the Mermaid source during playback", async () => {
+ const content = await renderDescription(
+ CAPTIONED_FLOWCHART_DESCRIPTION,
+ "light"
+ );
+
+ assert.ok(!content.includes("```mermaid"));
+ assert.ok(!content.includes("flowchart TD"));
+ assert.ok(!content.includes("Client --> Gateway"));
+});
+
+test("renderDescription returns descriptions without Mermaid unchanged", async () => {
+ const description = [
+ "A plain description with text.",
+ "",
+ "```ts",
+ "const answer = 42;",
+ "```",
+ "",
+ "[Next tour](command:codetour.startTourByTitle?%5B%22Next%22%5D)"
+ ].join("\n");
+
+ assert.equal(await renderDescription(description, "light"), description);
+ assert.equal(await renderDescription(description, "dark"), description);
+});
+
+test("makeRenderedImagesResponsive scales rendered PNGs to their container", () => {
+ const content =
+ "![Diagram — Reads \\[docs\\]](data:image/png;base64,iVBORw0KGgo=)";
+
+ assert.equal(
+ makeRenderedImagesResponsive(content),
+ '
'
+ );
+});
+
+test("makeRenderedImagesResponsive escapes HTML-sensitive alt text", () => {
+ const content =
+ '';
+
+ assert.equal(
+ makeRenderedImagesResponsive(content),
+ '
'
+ );
+});
+
+test("makeRenderedImagesResponsive scales ordinary image links too", () => {
+ const content = "";
+
+ assert.equal(
+ makeRenderedImagesResponsive(content),
+ '
'
+ );
+});
+
+test("makeRenderedImagesResponsive preserves image titles", () => {
+ const content = '';
+
+ assert.equal(
+ makeRenderedImagesResponsive(content),
+ '
'
+ );
+});
+
+test("makeRenderedImagesResponsive does not rewrite PNGs inside code fences", () => {
+ const content = [
+ "```markdown",
+ "",
+ "```"
+ ].join("\n");
+
+ assert.equal(makeRenderedImagesResponsive(content), content);
+});
+
+test("renderDescription renders every allowed diagram kind through the seam", async () => {
+ for (const [kind, source] of Object.entries(ALLOWED_KIND_SOURCES)) {
+ const caption = ALLOWED_KIND_CAPTIONS[kind];
+ const content = await renderDescription(captionedDiagram(caption, source), "light");
+
+ const png = extractPngDataUri(content, caption);
+ assertValidPng(png);
+ assert.ok(content.includes(`**${caption}**`), kind);
+ assert.ok(!content.includes("```mermaid"), kind);
+ assert.ok(!content.includes(source), kind);
+ }
+});
+
+test("renderDescription renders a sequence diagram with message arrows", async () => {
+ const content = await renderDescription(
+ captionedDiagram(
+ ALLOWED_KIND_CAPTIONS.sequenceDiagram,
+ ALLOWED_KIND_SOURCES.sequenceDiagram
+ ),
+ "light"
+ );
+
+ assertValidPng(
+ extractPngDataUri(content, ALLOWED_KIND_CAPTIONS.sequenceDiagram)
+ );
+});
+
+test("renderDescription renders a class diagram with members", async () => {
+ const content = await renderDescription(
+ captionedDiagram(
+ ALLOWED_KIND_CAPTIONS.classDiagram,
+ ALLOWED_KIND_SOURCES.classDiagram
+ ),
+ "light"
+ );
+
+ assertValidPng(
+ extractPngDataUri(content, ALLOWED_KIND_CAPTIONS.classDiagram)
+ );
+});
+
+test("renderDescription fails an unsupported kind locally, keeping the caption as alternative text", async () => {
+ for (const [label, source] of Object.entries(UNSUPPORTED_KIND_SOURCES)) {
+ const caption = `Diagram — ${label} attempt`;
+ const content = await renderDescription(captionedDiagram(caption, source), "light");
+
+ assertNotice(content, KIND_NOTICE_MARKER);
+ assert.ok(content.includes(`**${caption}**`), label);
+ assert.ok(!content.includes("```mermaid"), label);
+ assert.ok(!content.includes(source), label);
+ }
+});
+
+test("renderDescription fails a Mermaid fence without a caption, without exposing its source", async () => {
+ const description = [
+ "Some text that is not a caption.",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```"
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"));
+ assert.ok(!content.includes(FLOWCHART_SOURCE));
+});
+
+test("renderDescription fails a Mermaid fence whose caption is malformed", async () => {
+ for (const captionLine of [
+ "*Diagram — single stars*",
+ "**Diagram – wrong dash**",
+ "**Diagram —**",
+ "**Diagram — a** and **b**"
+ ]) {
+ const description = [captionLine, "", "```mermaid", FLOWCHART_SOURCE, "```"].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"), captionLine);
+ assert.ok(!content.includes(FLOWCHART_SOURCE), captionLine);
+ }
+});
+
+test("renderDescription fails a Mermaid fence with markdown between the caption and the fence", async () => {
+ const description = [
+ "**Diagram — Interrupted**",
+ "",
+ "Read this first:",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```"
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"));
+ assert.ok(!content.includes(FLOWCHART_SOURCE));
+});
+
+test("a caption can only introduce a single fence", async () => {
+ const description = [
+ "**Diagram — First**",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```"
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertValidPng(extractPngDataUri(content, "Diagram — First"));
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"));
+});
+
+test("renderDescription fails a diagram source over 20 KB", async () => {
+ const source = flowchartOfExactByteLength(20 * 1024 + 1);
+ const content = await renderDescription(
+ captionedDiagram("Diagram — Too big", source),
+ "light"
+ );
+
+ assertNotice(content, SIZE_NOTICE_MARKER);
+ assert.ok(content.includes("**Diagram — Too big**"));
+ assert.ok(!content.includes("```mermaid"));
+ assert.ok(!content.includes(source.slice(0, 200)));
+});
+
+test("renderDescription renders a diagram source of exactly 20 KB", async () => {
+ const source = flowchartOfExactByteLength(20 * 1024);
+ const content = await renderDescription(
+ captionedDiagram("Diagram — Boundary", source),
+ "light"
+ );
+
+ assertValidPng(extractPngDataUri(content, "Diagram — Boundary"));
+});
+
+test("renderDescription renders at most three diagrams and fails the excess locally", async () => {
+ const description = [1, 2, 3, 4, 5]
+ .map(number =>
+ captionedDiagram(`Diagram — Chart ${number}`, FLOWCHART_SOURCE)
+ )
+ .join("\n\n");
+
+ const content = await renderDescription(description, "light");
+
+ for (const number of [1, 2, 3]) {
+ assertValidPng(
+ extractPngDataUri(content, `Diagram — Chart ${number}`)
+ );
+ }
+ const excessNotices = content
+ .split("\n")
+ .filter(line => line.includes(COUNT_NOTICE_MARKER));
+ assert.equal(excessNotices.length, 2);
+ assert.ok(content.includes("**Diagram — Chart 4**"));
+ assert.ok(content.includes("**Diagram — Chart 5**"));
+ assert.ok(!content.includes("```mermaid"));
+});
+
+test("the first three fences count toward the limit even when they fail other rules", async () => {
+ const description = [
+ "Intro text",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```",
+ "",
+ captionedDiagram("Diagram — Unsupported", UNSUPPORTED_KIND_SOURCES.pie),
+ captionedDiagram("Diagram — Valid", FLOWCHART_SOURCE),
+ captionedDiagram("Diagram — Fourth", FLOWCHART_SOURCE)
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertValidPng(extractPngDataUri(content, "Diagram — Valid"));
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assertNotice(content, KIND_NOTICE_MARKER);
+ assertNotice(content, COUNT_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"));
+});
+
+test("renderDescription replaces an invalid diagram with a warning, not its source", async () => {
+ const description = [
+ "**Diagram — Broken diagram**",
+ "",
+ "```mermaid",
+ "flowchart TD",
+ " this is not mermaid at all (((",
+ "```"
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assert.ok(!content.includes("```mermaid"));
+ assert.ok(!content.includes("this is not mermaid at all"));
+ assertNotice(content, RENDER_NOTICE_MARKER);
+ assert.ok(content.includes("**Diagram — Broken diagram**"));
+});
+
+test("renderDescription renders diagrams in one description independently", async () => {
+ const description = [
+ "**Diagram — Valid diagram**",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```",
+ "",
+ "**Diagram — Broken diagram**",
+ "",
+ "```mermaid",
+ "flowchart TD",
+ " this is not mermaid at all (((",
+ "```"
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertValidPng(extractPngDataUri(content, "Diagram — Valid diagram"));
+ assertNotice(content, RENDER_NOTICE_MARKER);
+ assert.ok(!content.includes("this is not mermaid at all"));
+});
+
+test("one rejected diagram never hides its valid siblings", async () => {
+ const description = [
+ "Plain text above",
+ "",
+ "```mermaid",
+ FLOWCHART_SOURCE,
+ "```",
+ "",
+ captionedDiagram("Diagram — Supported", FLOWCHART_SOURCE),
+ captionedDiagram("Diagram — Unsupported", UNSUPPORTED_KIND_SOURCES.gitGraph)
+ ].join("\n");
+
+ const content = await renderDescription(description, "light");
+
+ assertValidPng(extractPngDataUri(content, "Diagram — Supported"));
+ assertNotice(content, CAPTION_NOTICE_MARKER);
+ assertNotice(content, KIND_NOTICE_MARKER);
+ assert.ok(!content.includes("```mermaid"));
+});
+
+test("renderDescription adapts the rendered diagram to the theme", async () => {
+ const light = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+ const dark = await renderMermaidDiagram(FLOWCHART_SOURCE, "dark");
+
+ assertValidPng(light.png);
+ assertValidPng(dark.png);
+ assert.notEqual(light.svg, dark.svg);
+ assert.ok(Buffer.compare(light.png, dark.png) !== 0);
+});
+
+test("renderMermaidDiagram returns a sanitized SVG and an in-memory PNG", async () => {
+ const { svg, png } = await renderMermaidDiagram(FLOWCHART_SOURCE, "light");
+
+ assert.ok(svg.startsWith(""
+ ].join("");
+
+ const sanitized = sanitizeSvg(hostile);
+
+ assert.ok(!sanitized.includes("Bad link',
+ "Tour description",
+ "test-nonce"
+ );
+
+ assert.doesNotMatch(html, /alert\("unsafe"\)/);
+ assert.doesNotMatch(html, /href="javascript:/);
+ assert.match(html, />Bad link<\/a>/);
+});
diff --git a/src/player/descriptionWebview.ts b/src/player/descriptionWebview.ts
new file mode 100644
index 00000000..a23bce03
--- /dev/null
+++ b/src/player/descriptionWebview.ts
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { randomBytes } from "crypto";
+import * as vscode from "vscode";
+import { store } from "../store";
+import { getStepLabel } from "../utils";
+import { renderPreviewDescription } from "./description";
+import { createDescriptionWebviewHtml } from "./markdown";
+
+const VIEW_TYPE = "codetour.description";
+const ALLOWED_COMMANDS = /^(codetour\.|vscode\.open$)/;
+
+let panel: vscode.WebviewPanel | undefined;
+let linkWorkspaceRoot: vscode.Uri | undefined;
+
+function reviveCommandArgument(value: unknown): unknown {
+ if (
+ value &&
+ typeof value === "object" &&
+ "scheme" in value &&
+ "path" in value
+ ) {
+ const components = value as {
+ scheme: string;
+ authority?: string;
+ path?: string;
+ query?: string;
+ fragment?: string;
+ };
+ return vscode.Uri.from({
+ scheme: components.scheme,
+ authority: components.authority,
+ path: components.path,
+ query: components.query,
+ fragment: components.fragment
+ });
+ }
+
+ return value;
+}
+
+async function openLink(
+ href: string,
+ workspaceRoot?: vscode.Uri
+): Promise {
+ const uri = vscode.Uri.parse(href, true);
+ if (uri.scheme === "command" && ALLOWED_COMMANDS.test(uri.path)) {
+ const parsedArgs: unknown = uri.query
+ ? JSON.parse(decodeURIComponent(uri.query))
+ : [];
+ const args = (Array.isArray(parsedArgs) ? parsedArgs : [parsedArgs]).map(
+ reviveCommandArgument
+ );
+ await vscode.commands.executeCommand(uri.path, ...args);
+ } else if (["http", "https", "mailto"].includes(uri.scheme)) {
+ await vscode.env.openExternal(uri);
+ } else if (!uri.scheme && workspaceRoot) {
+ await vscode.commands.executeCommand(
+ "vscode.open",
+ vscode.Uri.joinPath(workspaceRoot, uri.path)
+ );
+ }
+}
+
+export async function showStepDescription(): Promise {
+ const activeTour = store.activeTour;
+ if (!activeTour) {
+ return;
+ }
+
+ const { tour, step, workspaceRoot } = activeTour;
+ const currentStep = tour.steps[step];
+ if (!currentStep?.description) {
+ return;
+ }
+
+ const title = `Description — ${getStepLabel(tour, step)}`;
+ const content = await renderPreviewDescription(currentStep.description, undefined, {
+ tour,
+ tours: activeTour.tours,
+ workspaceRoot
+ });
+
+ if (!panel) {
+ panel = vscode.window.createWebviewPanel(
+ VIEW_TYPE,
+ title,
+ vscode.ViewColumn.Beside,
+ { enableScripts: true }
+ );
+ panel.onDidDispose(() => {
+ panel = undefined;
+ linkWorkspaceRoot = undefined;
+ });
+ panel.webview.onDidReceiveMessage(async message => {
+ if (message?.type === "openLink" && typeof message.href === "string") {
+ try {
+ await openLink(message.href, linkWorkspaceRoot);
+ } catch {
+ vscode.window.showErrorMessage("Unable to open this description link.");
+ }
+ }
+ });
+ } else {
+ panel.reveal(vscode.ViewColumn.Beside);
+ panel.title = title;
+ }
+
+ linkWorkspaceRoot = workspaceRoot;
+ panel.webview.html = createDescriptionWebviewHtml(
+ content,
+ title,
+ randomBytes(16).toString("base64")
+ );
+}
diff --git a/src/player/index.ts b/src/player/index.ts
index 2e5935b0..c32483f3 100644
--- a/src/player/index.ts
+++ b/src/player/index.ts
@@ -21,6 +21,7 @@ import {
window,
workspace
} from "vscode";
+import { clearMermaidRenderCache } from "codetour-description-renderer";
import { SMALL_ICON_URL } from "../constants";
import { CodeTour, store } from "../store";
import { initializeStorage } from "../store/storage";
@@ -37,6 +38,8 @@ import { registerPlayerCommands } from "./commands";
import { registerDecorators } from "./decorator";
import { registerFileSystemProvider } from "./fileSystem";
import { registerTextDocumentContentProvider } from "./fileSystem/documentProvider";
+import { renderPreviewDescription } from "./description";
+import { appendCommentNavigation } from "./navigation";
import { registerStatusBar } from "./status";
import { registerTreeProvider } from "./tree";
@@ -44,71 +47,9 @@ const CONTROLLER_ID = "codetour";
const CONTROLLER_LABEL = "CodeTour";
let id = 0;
+let renderRequest = 0;
-const SHELL_SCRIPT_PATTERN = /^>>\s+(?
+