fix(workspace): stream incremental markdown rendering

This commit is contained in:
2026-09-18 18:35:51 +09:00
parent 1b153ba144
commit 092aea62d2
12 changed files with 548 additions and 318 deletions
+3 -2
View File
@@ -6,7 +6,8 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview"
},
@@ -27,7 +28,7 @@
"cookie": "npm:cookie@0.6.0",
"devalue": "npm:devalue@5.6.4",
"set-cookie-parser": "npm:set-cookie-parser@2.7.2",
"shiki": "npm:shiki@3.13.0",
"shiki": "npm:shiki@4.4.2",
"svelte": "npm:svelte@5.45.6",
"svelte-check": "npm:svelte-check@4.3.4",
"typescript": "npm:typescript@5.9.3",
+397 -23
View File
@@ -9,23 +9,27 @@
"npm:@codemirror/state@6.7.1": "6.7.1",
"npm:@codemirror/view@6.43.8": "6.43.8",
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
"npm:@humanspeak/svelte-markdown@1.9.1": "1.9.1_shiki@4.4.2_svelte@5.45.6",
"npm:@lezer/common@1.5.2": "1.5.2",
"npm:@lezer/highlight@1.2.3": "1.2.3",
"npm:@sveltejs/adapter-static@3.0.9": "3.0.9_@sveltejs+kit@2.49.4__@sveltejs+vite-plugin-svelte@6.2.1___svelte@5.45.6___vite@7.2.7____yaml@2.9.0___yaml@2.9.0__svelte@5.45.6__typescript@5.9.3__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_yaml@2.9.0",
"npm:@sveltejs/kit@2.49.4": "2.49.4_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.45.6__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_svelte@5.45.6_typescript@5.9.3_vite@7.2.7__yaml@2.9.0_yaml@2.9.0",
"npm:@sveltejs/vite-plugin-svelte@6.2.1": "6.2.1_svelte@5.45.6_vite@7.2.7__yaml@2.9.0_yaml@2.9.0",
"npm:@testing-library/svelte@5.2.8": "5.2.8_svelte@5.45.6_vite@7.2.7__yaml@2.9.0_vitest@3.2.4__happy-dom@18.0.1_happy-dom@18.0.1",
"npm:clsx@2.1.1": "2.1.1",
"npm:cookie@0.6.0": "0.6.0",
"npm:decodal-codemirror@0.3.0": "0.3.0_@codemirror+language@6.12.4_@codemirror+view@6.43.8_@lezer+highlight@1.2.3_@lezer+lr@1.4.10",
"npm:devalue@5.6.4": "5.6.4",
"npm:gen-interface-jp@0.8.0": "0.8.0",
"npm:happy-dom@18.0.1": "18.0.1",
"npm:set-cookie-parser@2.7.2": "2.7.2",
"npm:shiki@3.13.0": "3.13.0",
"npm:shiki@4.4.2": "4.4.2",
"npm:sv@*": "0.10.8",
"npm:svelte-check@4.3.4": "4.3.4_svelte@5.45.6_typescript@5.9.3",
"npm:svelte@5.45.6": "5.45.6",
"npm:typescript@5.9.3": "5.9.3",
"npm:vite@7.2.7": "7.2.7_yaml@2.9.0",
"npm:vitest@3.2.4": "3.2.4_happy-dom@18.0.1",
"npm:yaml@2.9.0": "2.9.0"
},
"jsr": {
@@ -40,6 +44,20 @@
}
},
"npm": {
"@babel/code-frame@7.29.7": {
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dependencies": [
"@babel/helper-validator-identifier",
"js-tokens@4.0.0",
"picocolors"
]
},
"@babel/helper-validator-identifier@7.29.7": {
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="
},
"@babel/runtime@7.29.7": {
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="
},
"@codemirror/autocomplete@6.20.0": {
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"dependencies": [
@@ -217,6 +235,23 @@
"@fontsource/ibm-plex-mono@5.3.0": {
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q=="
},
"@humanspeak/memory-cache@1.2.0": {
"integrity": "sha512-im7GIIpgjdD6Fx68xCeiXw3XiDv30/WMRAaodMm+ep0ptYfEUd0cdNeTAylHGIRTJIeByKFHdTmPjAktyDM/JQ=="
},
"@humanspeak/svelte-markdown@1.9.1_shiki@4.4.2_svelte@5.45.6": {
"integrity": "sha512-2JjpATdZfhGQurV38LtkUkPkyQzM7eXku6LoDBeK6i69qqOe0OLmMwpqtMnhwG3ItdxXOt/3miPbSaNTFqMl5Q==",
"dependencies": [
"@humanspeak/memory-cache",
"github-slugger",
"htmlparser2",
"marked",
"shiki",
"svelte"
],
"optionalPeers": [
"shiki"
]
},
"@jridgewell/gen-mapping@0.3.13": {
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dependencies": [
@@ -390,44 +425,53 @@
"os": ["win32"],
"cpu": ["x64"]
},
"@shikijs/core@3.13.0": {
"integrity": "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==",
"@shikijs/core@4.4.2": {
"integrity": "sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==",
"dependencies": [
"@shikijs/primitive",
"@shikijs/types",
"@shikijs/vscode-textmate",
"@types/hast",
"hast-util-to-html"
]
},
"@shikijs/engine-javascript@3.13.0": {
"integrity": "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==",
"@shikijs/engine-javascript@4.4.2": {
"integrity": "sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==",
"dependencies": [
"@shikijs/types",
"@shikijs/vscode-textmate",
"oniguruma-to-es"
]
},
"@shikijs/engine-oniguruma@3.13.0": {
"integrity": "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==",
"@shikijs/engine-oniguruma@4.4.2": {
"integrity": "sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==",
"dependencies": [
"@shikijs/types",
"@shikijs/vscode-textmate"
]
},
"@shikijs/langs@3.13.0": {
"integrity": "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==",
"@shikijs/langs@4.4.2": {
"integrity": "sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==",
"dependencies": [
"@shikijs/types"
]
},
"@shikijs/themes@3.13.0": {
"integrity": "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==",
"@shikijs/primitive@4.4.2": {
"integrity": "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==",
"dependencies": [
"@shikijs/types",
"@shikijs/vscode-textmate",
"@types/hast"
]
},
"@shikijs/themes@4.4.2": {
"integrity": "sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==",
"dependencies": [
"@shikijs/types"
]
},
"@shikijs/types@3.13.0": {
"integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==",
"@shikijs/types@4.4.2": {
"integrity": "sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==",
"dependencies": [
"@shikijs/vscode-textmate",
"@types/hast"
@@ -498,14 +542,53 @@
"vitefu"
]
},
"@testing-library/dom@10.4.2": {
"integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
"dependencies": [
"@babel/code-frame",
"@babel/runtime",
"@types/aria-query",
"aria-query@5.3.0",
"dom-accessibility-api",
"lz-string",
"picocolors",
"pretty-format"
]
},
"@testing-library/svelte@5.2.8_svelte@5.45.6_vite@7.2.7__yaml@2.9.0_vitest@3.2.4__happy-dom@18.0.1_happy-dom@18.0.1": {
"integrity": "sha512-ucQOtGsJhtawOEtUmbR4rRh53e6RbM1KUluJIXRmh6D4UzxR847iIqqjRtg9mHNFmGQ8Vkam9yVcR5d1mhIHKA==",
"dependencies": [
"@testing-library/dom",
"svelte",
"vite",
"vitest"
],
"optionalPeers": [
"vite",
"vitest"
]
},
"@types/aria-query@5.0.4": {
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="
},
"@types/chai@5.2.3": {
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dependencies": [
"@types/deep-eql",
"assertion-error"
]
},
"@types/cookie@0.6.0": {
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"@types/deep-eql@4.0.2": {
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="
},
"@types/estree@1.0.8": {
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
},
"@types/hast@3.0.4": {
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"@types/hast@3.0.5": {
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
"dependencies": [
"@types/unist"
]
@@ -516,9 +599,18 @@
"@types/unist"
]
},
"@types/node@20.19.43": {
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dependencies": [
"undici-types"
]
},
"@types/unist@3.0.3": {
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
"@types/whatwg-mimetype@3.0.2": {
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="
},
"@typescript-eslint/types@8.57.2": {
"integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="
},
@@ -526,25 +618,114 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"deprecated": true
},
"@vitest/expect@3.2.4": {
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dependencies": [
"@types/chai",
"@vitest/spy",
"@vitest/utils",
"chai",
"tinyrainbow"
]
},
"@vitest/mocker@3.2.4_vite@7.2.7__yaml@2.9.0": {
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dependencies": [
"@vitest/spy",
"estree-walker",
"magic-string",
"vite"
],
"optionalPeers": [
"vite"
]
},
"@vitest/pretty-format@3.2.4": {
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dependencies": [
"tinyrainbow"
]
},
"@vitest/runner@3.2.4": {
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"dependencies": [
"@vitest/utils",
"pathe",
"strip-literal"
]
},
"@vitest/snapshot@3.2.4": {
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"dependencies": [
"@vitest/pretty-format",
"magic-string",
"pathe"
]
},
"@vitest/spy@3.2.4": {
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dependencies": [
"tinyspy"
]
},
"@vitest/utils@3.2.4": {
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dependencies": [
"@vitest/pretty-format",
"loupe",
"tinyrainbow"
]
},
"acorn@8.16.0": {
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"bin": true
},
"ansi-regex@5.0.1": {
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles@5.2.0": {
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
},
"aria-query@5.3.0": {
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dependencies": [
"dequal"
]
},
"aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
},
"assertion-error@2.0.1": {
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="
},
"axobject-query@4.1.0": {
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="
},
"cac@6.7.14": {
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="
},
"ccount@2.0.1": {
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="
},
"chai@5.3.3": {
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dependencies": [
"assertion-error",
"check-error",
"deep-eql",
"loupe",
"pathval"
]
},
"character-entities-html4@2.1.0": {
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="
},
"character-entities-legacy@3.0.0": {
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="
},
"check-error@2.1.3": {
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="
},
"chokidar@4.0.3": {
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dependencies": [
@@ -578,6 +759,9 @@
"@lezer/lr"
]
},
"deep-eql@5.0.2": {
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="
},
"deepmerge@4.3.1": {
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
@@ -593,6 +777,40 @@
"dequal"
]
},
"dom-accessibility-api@0.5.16": {
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
},
"dom-serializer@3.1.1": {
"integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
"dependencies": [
"domelementtype",
"domhandler",
"entities"
]
},
"domelementtype@3.0.0": {
"integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="
},
"domhandler@6.0.1": {
"integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
"dependencies": [
"domelementtype"
]
},
"domutils@4.0.2": {
"integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
"dependencies": [
"dom-serializer",
"domelementtype",
"domhandler"
]
},
"entities@8.1.0": {
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="
},
"es-module-lexer@1.7.0": {
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="
},
"esbuild@0.25.12": {
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"optionalDependencies": [
@@ -636,6 +854,15 @@
"@typescript-eslint/types"
]
},
"estree-walker@3.0.3": {
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": [
"@types/estree"
]
},
"expect-type@1.4.0": {
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="
},
"fdir@6.5.0_picomatch@4.0.4": {
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dependencies": [
@@ -653,6 +880,17 @@
"gen-interface-jp@0.8.0": {
"integrity": "sha512-7rIytSAfuugVjYithrQseT0zqeUIkJd0LY3Ugdfbk/FxzMSeagcskeZlof5uWJ5liW3Zhn7pdLHq2OIvAftx+A=="
},
"github-slugger@2.0.0": {
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
},
"happy-dom@18.0.1": {
"integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==",
"dependencies": [
"@types/node",
"@types/whatwg-mimetype",
"whatwg-mimetype"
]
},
"hast-util-to-html@9.0.5": {
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"dependencies": [
@@ -678,24 +916,50 @@
"html-void-elements@3.0.0": {
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="
},
"htmlparser2@12.0.0": {
"integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==",
"dependencies": [
"domelementtype",
"domhandler",
"domutils",
"entities"
]
},
"is-reference@3.0.3": {
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dependencies": [
"@types/estree"
]
},
"js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-tokens@9.0.1": {
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="
},
"kleur@4.1.5": {
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="
},
"locate-character@3.0.0": {
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
},
"loupe@3.2.1": {
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="
},
"lz-string@1.5.0": {
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"bin": true
},
"magic-string@0.30.21": {
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dependencies": [
"@jridgewell/sourcemap-codec"
]
},
"marked@18.0.13": {
"integrity": "sha512-xTxVzZsBFwunP6HDmtBkabUQEYArnP7/rMDGmPj9SlrKlQ4i8MdYVow+nJL0eOqwpUqhzBoTBRADGN6uYwPyOw==",
"bin": true
},
"mdast-util-to-hast@13.2.1": {
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"dependencies": [
@@ -750,17 +1014,23 @@
"obug@2.1.1": {
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="
},
"oniguruma-parser@0.12.1": {
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="
"oniguruma-parser@0.12.2": {
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="
},
"oniguruma-to-es@4.3.4": {
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==",
"oniguruma-to-es@4.3.6": {
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
"dependencies": [
"oniguruma-parser",
"regex",
"regex-recursion"
]
},
"pathe@2.0.3": {
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
},
"pathval@2.0.1": {
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
@@ -775,9 +1045,20 @@
"source-map-js"
]
},
"pretty-format@27.5.1": {
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dependencies": [
"ansi-regex",
"ansi-styles",
"react-is"
]
},
"property-information@7.1.0": {
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="
},
"react-is@17.0.2": {
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"readdirp@4.1.2": {
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
},
@@ -840,8 +1121,8 @@
"set-cookie-parser@2.7.2": {
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
},
"shiki@3.13.0": {
"integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==",
"shiki@4.4.2": {
"integrity": "sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==",
"dependencies": [
"@shikijs/core",
"@shikijs/engine-javascript",
@@ -853,6 +1134,9 @@
"@types/hast"
]
},
"siginfo@2.0.0": {
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="
},
"sirv@3.0.2": {
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"dependencies": [
@@ -867,6 +1151,12 @@
"space-separated-tokens@2.0.2": {
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="
},
"stackback@0.0.2": {
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="
},
"std-env@3.10.0": {
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="
},
"stringify-entities@4.0.4": {
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"dependencies": [
@@ -874,6 +1164,12 @@
"character-entities-legacy"
]
},
"strip-literal@3.1.0": {
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dependencies": [
"js-tokens@9.0.1"
]
},
"style-mod@4.1.3": {
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="
},
@@ -902,7 +1198,7 @@
"@sveltejs/acorn-typescript",
"@types/estree",
"acorn",
"aria-query",
"aria-query@5.3.2",
"axobject-query",
"clsx",
"devalue",
@@ -914,6 +1210,12 @@
"zimmerframe"
]
},
"tinybench@2.9.0": {
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="
},
"tinyexec@0.3.2": {
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="
},
"tinyglobby@0.2.15": {
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dependencies": [
@@ -921,6 +1223,15 @@
"picomatch"
]
},
"tinypool@1.1.1": {
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="
},
"tinyrainbow@2.0.0": {
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="
},
"tinyspy@4.0.6": {
"integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="
},
"totalist@3.0.1": {
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
},
@@ -931,6 +1242,9 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"bin": true
},
"undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"unist-util-is@6.0.1": {
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"dependencies": [
@@ -978,6 +1292,17 @@
"vfile-message"
]
},
"vite-node@3.2.4": {
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dependencies": [
"cac",
"debug",
"es-module-lexer",
"pathe",
"vite"
],
"bin": true
},
"vite@7.2.7_yaml@2.9.0": {
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dependencies": [
@@ -1006,9 +1331,53 @@
"vite"
]
},
"vitest@3.2.4_happy-dom@18.0.1": {
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dependencies": [
"@types/chai",
"@vitest/expect",
"@vitest/mocker",
"@vitest/pretty-format",
"@vitest/runner",
"@vitest/snapshot",
"@vitest/spy",
"@vitest/utils",
"chai",
"debug",
"expect-type",
"happy-dom",
"magic-string",
"pathe",
"picomatch",
"std-env",
"tinybench",
"tinyexec",
"tinyglobby",
"tinypool",
"tinyrainbow",
"vite",
"vite-node",
"why-is-node-running"
],
"optionalPeers": [
"happy-dom"
],
"bin": true
},
"w3c-keyname@2.2.8": {
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"whatwg-mimetype@3.0.0": {
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="
},
"why-is-node-running@2.3.0": {
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dependencies": [
"siginfo",
"stackback"
],
"bin": true
},
"yaml@2.9.0": {
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"bin": true
@@ -1037,7 +1406,7 @@
"npm:decodal-codemirror@0.3.0",
"npm:devalue@5.6.4",
"npm:set-cookie-parser@2.7.2",
"npm:shiki@3.13.0",
"npm:shiki@4.4.2",
"npm:svelte-check@4.3.4",
"npm:svelte@5.45.6",
"npm:typescript@5.9.3",
@@ -1046,7 +1415,12 @@
"packageJson": {
"dependencies": [
"npm:@fontsource/ibm-plex-mono@5.3.0",
"npm:@humanspeak/svelte-markdown@1.9.1",
"npm:@testing-library/svelte@5.2.8",
"npm:gen-interface-jp@0.8.0",
"npm:happy-dom@18.0.1",
"npm:shiki@4.4.2",
"npm:vitest@3.2.4",
"npm:yaml@2.9.0"
]
}
+7
View File
@@ -5,7 +5,14 @@
"type": "module",
"dependencies": {
"@fontsource/ibm-plex-mono": "5.3.0",
"@humanspeak/svelte-markdown": "1.9.1",
"gen-interface-jp": "0.8.0",
"shiki": "4.4.2",
"yaml": "2.9.0"
},
"devDependencies": {
"@testing-library/svelte": "5.2.8",
"happy-dom": "18.0.1",
"vitest": "3.2.4"
}
}
@@ -133,7 +133,7 @@
{:else if item.kind === 'user'}
<div class="user-message">
<span class="user-prompt" aria-hidden="true">&gt;</span>
<div><RichMarkdown text={item.body || '—'} /></div>
<div><RichMarkdown text={item.body || '—'} streamId={item.id} /></div>
</div>
{:else if item.kind === 'activity'}
<p class="activity-summary">{item.body || '—'}</p>
@@ -142,7 +142,7 @@
{:else if item.kind === 'run_stats'}
<p class="run-stats">{item.body}</p>
{:else if shouldRenderMarkdown(item)}
<RichMarkdown text={item.body || '—'} />
<RichMarkdown text={item.body || '—'} streamId={item.id} />
{:else}
<p class="console-plain-text">{item.body || '—'}</p>
{/if}
@@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from "svelte";
let {
href = "",
title,
children,
}: {
href?: string;
title?: string;
children?: Snippet;
} = $props();
</script>
<a {href} {title} target="_blank" rel="noreferrer">
{@render children?.()}
</a>
@@ -0,0 +1,70 @@
// @vitest-environment happy-dom
import { cleanup, render, waitFor } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import RichMarkdown from "./RichMarkdown.svelte";
afterEach(cleanup);
test("preserves safe links, blocks raw HTML, and renders Shiki code", async () => {
const source = [
"[unsafe](javascript:alert(1)) and [safe](https://example.com)",
"",
'<img src="x" onerror="alert(1)">',
"",
"```ts",
"const answer: number = 42;",
"```",
].join("\n");
const { container } = render(RichMarkdown, {
text: source,
streamId: "message-security",
});
await waitFor(() => {
expect(container.querySelectorAll("a")).toHaveLength(2);
expect(container.querySelector("pre.shiki code span")).not.toBeNull();
});
const [unsafe, safe] = container.querySelectorAll("a");
expect(unsafe.getAttribute("href")).toBe("");
expect(safe.getAttribute("href")).toBe("https://example.com");
expect(safe.getAttribute("target")).toBe("_blank");
expect(safe.getAttribute("rel")).toBe("noreferrer");
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("script")).toBeNull();
});
test("keeps completed DOM nodes while a later markdown block grows", async () => {
const initial = [
"# Stable heading",
"",
"This paragraph is already complete.",
"",
"The final paragraph is stream",
].join("\n");
const { container, rerender } = render(RichMarkdown, {
text: initial,
streamId: "message-1",
});
await waitFor(() => {
expect(container.querySelector("h1")?.textContent).toBe("Stable heading");
expect(container.querySelectorAll("p")).toHaveLength(2);
});
const heading = container.querySelector("h1");
const completedParagraph = container.querySelector("p");
await rerender({
text: `${initial}ed incrementally.`,
streamId: "message-1",
});
await waitFor(() => {
expect(container.querySelectorAll("p")[1]?.textContent).toBe(
"The final paragraph is streamed incrementally.",
);
});
expect(container.querySelector("h1")).toBe(heading);
expect(container.querySelector("p")).toBe(completedParagraph);
});
@@ -1,41 +1,49 @@
<script module lang="ts">
import {
createShikiHighlighter,
setShikiHighlighter,
} from "@humanspeak/svelte-markdown/extensions/shiki";
import bash from "shiki/langs/bash.mjs";
import javascript from "shiki/langs/javascript.mjs";
import json from "shiki/langs/json.mjs";
import python from "shiki/langs/python.mjs";
import rust from "shiki/langs/rust.mjs";
import typescript from "shiki/langs/typescript.mjs";
import kanagawaWave from "shiki/themes/kanagawa-wave.mjs";
setShikiHighlighter(
createShikiHighlighter({
themes: [kanagawaWave],
langs: [bash, javascript, json, python, rust, typescript],
}),
);
</script>
<script lang="ts">
import { markdownToHtml } from "$lib/workspace/console/markdown";
import SvelteMarkdown, {
buildUnsupportedHTML,
type Renderers,
} from "@humanspeak/svelte-markdown";
import { ShikiCode } from "@humanspeak/svelte-markdown/extensions/shiki";
import MarkdownLink from "$lib/workspace/console/MarkdownLink.svelte";
type Props = {
text: string;
streamId?: string | number;
class?: string;
};
let { text, class: className = "" }: Props = $props();
let html = $state("");
let rendering = $state(false);
let { text, streamId = "static", class: className = "" }: Props = $props();
async function render(value: string): Promise<void> {
const current = value;
rendering = true;
try {
const next = await markdownToHtml(current);
if (text === current) {
html = next;
}
} finally {
if (text === current) {
rendering = false;
}
}
}
$effect(() => {
void render(text);
});
const renderers = {
code: ShikiCode,
html: buildUnsupportedHTML(),
link: MarkdownLink,
} satisfies Partial<Renderers>;
</script>
<div class={`rich-markdown ${className}`} class:is-rendering={rendering}>
{#if html}
{@html html}
{:else}
<p>{text}</p>
{/if}
<div class={`rich-markdown ${className}`}>
<SvelteMarkdown source={text} {streamId} {renderers} streaming />
</div>
<style>
@@ -1,36 +0,0 @@
import { markdownToHtml } from "./markdown.ts";
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
Deno.test("markdownToHtml escapes raw html and renders basic markdown", async () => {
const html = await markdownToHtml(
"# Title\n\nhello **world** `<x>`\n\n<script>bad()</script>",
);
assert(html.includes("<h1>Title</h1>"), "heading should render");
assert(html.includes("<strong>world</strong>"), "strong should render");
assert(
html.includes("&lt;script&gt;bad()&lt;/script&gt;"),
"raw html should be escaped",
);
assert(
!html.includes("<script>bad()</script>"),
"raw html must not pass through",
);
});
Deno.test("markdownToHtml renders fenced code through shiki", async () => {
const html = await markdownToHtml("```ts\nconst answer: number = 42;\n```");
assert(
html.includes("shiki"),
"highlighted code should include shiki markup",
);
assert(html.includes("answer"), "code content should be present");
});
@@ -1,225 +0,0 @@
import { codeToHtml } from "shiki";
const SHIKI_THEME = "kanagawa-wave";
export async function markdownToHtml(markdown: string): Promise<string> {
if (!markdown.trim()) {
return "";
}
const blocks = splitFencedCode(markdown);
const html: string[] = [];
let textBuffer: string[] = [];
async function flushText(): Promise<void> {
if (textBuffer.length === 0) {
return;
}
html.push(renderMarkdownText(textBuffer.join("\n")));
textBuffer = [];
}
for (const block of blocks) {
if (block.kind === "text") {
textBuffer.push(block.text);
continue;
}
await flushText();
html.push(await highlightCode(block.code, block.lang));
}
await flushText();
return html.join("\n");
}
type MarkdownBlock =
| { kind: "text"; text: string }
| { kind: "code"; lang: string; code: string };
function splitFencedCode(markdown: string): MarkdownBlock[] {
const lines = markdown.split(/\r?\n/);
const blocks: MarkdownBlock[] = [];
let text: string[] = [];
let code: string[] | null = null;
let lang = "text";
for (const line of lines) {
const fence = line.match(/^```\s*([^`]*)\s*$/);
if (fence) {
if (code) {
blocks.push({ kind: "text", text: text.join("\n") });
text = [];
blocks.push({ kind: "code", lang, code: code.join("\n") });
code = null;
lang = "text";
} else {
if (text.length > 0) {
blocks.push({ kind: "text", text: text.join("\n") });
text = [];
}
lang = normalizeLanguage(fence[1]);
code = [];
}
continue;
}
if (code) {
code.push(line);
} else {
text.push(line);
}
}
if (code) {
text.push("```" + (lang === "text" ? "" : lang));
text.push(...code);
}
if (text.length > 0) {
blocks.push({ kind: "text", text: text.join("\n") });
}
return blocks.filter((block) =>
block.kind === "code" || block.text.trim().length > 0
);
}
function renderMarkdownText(markdown: string): string {
const lines = markdown.split(/\r?\n/);
const html: string[] = [];
let paragraph: string[] = [];
let list: string[] = [];
let blockquote: string[] = [];
function flushParagraph(): void {
if (paragraph.length === 0) {
return;
}
html.push(`<p>${paragraph.map(renderInline).join("<br>")}</p>`);
paragraph = [];
}
function flushList(): void {
if (list.length === 0) {
return;
}
html.push(
`<ul>${
list.map((item) => `<li>${renderInline(item)}</li>`).join("")
}</ul>`,
);
list = [];
}
function flushBlockquote(): void {
if (blockquote.length === 0) {
return;
}
html.push(
`<blockquote>${blockquote.map(renderInline).join("<br>")}</blockquote>`,
);
blockquote = [];
}
function flushAll(): void {
flushParagraph();
flushList();
flushBlockquote();
}
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (!line.trim()) {
flushAll();
continue;
}
const heading = line.match(/^(#{1,4})\s+(.+)$/);
if (heading) {
flushAll();
const level = heading[1].length;
html.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
continue;
}
const listItem = line.match(/^\s*[-*]\s+(.+)$/);
if (listItem) {
flushParagraph();
flushBlockquote();
list.push(listItem[1]);
continue;
}
const quote = line.match(/^>\s?(.*)$/);
if (quote) {
flushParagraph();
flushList();
blockquote.push(quote[1]);
continue;
}
flushList();
flushBlockquote();
paragraph.push(line);
}
flushAll();
return html.join("\n");
}
function renderInline(value: string): string {
let escaped = escapeHtml(value);
escaped = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
escaped = escaped.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
escaped = escaped.replace(/\b(https?:\/\/[^\s<]+[^\s<.,;:)])/g, (url) => {
const safe = escapeAttribute(url);
return `<a href="${safe}" rel="noreferrer" target="_blank">${url}</a>`;
});
return escaped;
}
async function highlightCode(code: string, lang: string): Promise<string> {
try {
return await codeToHtml(code, {
lang: lang || "text",
theme: SHIKI_THEME,
});
} catch {
return `<pre class="shiki fallback"><code>${escapeHtml(code)}</code></pre>`;
}
}
function normalizeLanguage(value: string | undefined): string {
const raw = value?.trim().split(/\s+/, 1)[0] ?? "";
if (!raw) {
return "text";
}
switch (raw) {
case "sh":
case "shell":
case "zsh":
return "bash";
case "ts":
return "typescript";
case "js":
return "javascript";
default:
return raw;
}
}
function escapeHtml(value: string): string {
return value.replace(/[&<>"]/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case '"':
return "&quot;";
default:
return char;
}
});
}
function escapeAttribute(value: string): string {
return escapeHtml(value).replace(/'/g, "&#39;");
}
@@ -446,7 +446,9 @@ Deno.test("Worker Console renders markdown only for message rows", async () => {
consoleLine.includes("font-size: 12px;") &&
consoleLine.includes("line-height: 1.1;") &&
consoleLine.includes("{:else if shouldRenderMarkdown(item)}") &&
consoleLine.includes("<RichMarkdown text={item.body || '—'} />") &&
consoleLine.includes(
"<RichMarkdown text={item.body || '—'} streamId={item.id} />",
) &&
!consoleLine.includes("{@html"),
"Console should keep markdown rendering to message bodies, safely project Bash ANSI, and render other tool text literally",
);
+2 -1
View File
@@ -1,9 +1,10 @@
import { svelteTesting } from "@testing-library/svelte/vite";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { CODEMIRROR_VITE_DEDUPE } from "./src/lib/workspace/config-source/vite-dedupe";
export default defineConfig({
plugins: [sveltekit()],
plugins: [sveltekit(), svelteTesting()],
resolve: {
dedupe: CODEMIRROR_VITE_DEDUPE,
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config.ts";
export default mergeConfig(
viteConfig,
defineConfig({
test: {
include: ["src/**/*.browser.spec.ts"],
},
}),
);