feat(ui): add markdown preview to file viewer (#352)
Fixes #331 ## Summary - add an optional Markdown preview toggle for markdown files in the Files tab - add a word-wrap toggle for the source editor - escape raw HTML in preview mode and limit preview to plain Markdown file extensions ## Why The Files tab only showed raw source, which makes Markdown files harder to read quickly. This change adds a lightweight preview/source switch without introducing a larger viewer registry. ## What Changed - `packages/ui/src/components/instance/shell/right-panel/tabs/FilesTab.tsx` - added `Preview Markdown` / `Show source` toggle for markdown files - added a word-wrap toggle for the Monaco source viewer - restricted preview mode to plain Markdown extensions - escaped raw HTML in markdown preview mode - `packages/ui/src/components/file-viewer/monaco-file-viewer.tsx` - added configurable word-wrap support - `packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx` - moved file-viewer word-wrap state up so it persists across tab switches - `packages/ui/src/components/instance/shell/storage.ts` - added storage key for file-viewer word wrap - `packages/ui/src/lib/i18n/messages/*/instance.ts` - added strings for preview/source and word-wrap controls ## Validation - `npm run build --workspace @codenomad/ui`
This commit is contained in:
317
package-lock.json
generated
317
package-lock.json
generated
@@ -3189,84 +3189,6 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@neuralnomads/codenomad": {
|
||||
"resolved": "packages/server",
|
||||
"link": true
|
||||
@@ -3307,47 +3229,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.14.19",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.19.tgz",
|
||||
"integrity": "sha512-g0C8Viocybmet7nBqJK/1xrQnacRS1f30VmqRTPScPmWz+4knIZzc2TEQp8+920sN8rB6BuoGwfBUVRXJmavhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.14.19",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.101",
|
||||
"@opentui/solid": ">=0.1.101"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin/node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.14.19",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.19.tgz",
|
||||
"integrity": "sha512-9sTGsi8/HlBBeaWfsUjdJ2yi/SqpRvqSld0IFXc3ldaPb1w1uIPvgCGzhlHYQtqatXxSaX5lTN7zpudMaE21aw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin/node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
|
||||
"integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz",
|
||||
@@ -3952,12 +3833,6 @@
|
||||
"solid-js": "^1.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@suid/base": {
|
||||
"version": "0.11.0",
|
||||
"license": "MIT",
|
||||
@@ -6028,7 +5903,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6213,24 +6088,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "4.0.0-beta.48",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
|
||||
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-check": "^4.6.0",
|
||||
"find-my-way-ts": "^0.1.6",
|
||||
"ini": "^6.0.0",
|
||||
"kubernetes-types": "^1.30.0",
|
||||
"msgpackr": "^1.11.9",
|
||||
"multipasta": "^0.2.7",
|
||||
"toml": "^4.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"dev": true,
|
||||
@@ -6742,28 +6599,6 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
|
||||
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT"
|
||||
@@ -6994,12 +6829,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/find-my-way-ts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"license": "MIT",
|
||||
@@ -7809,15 +7638,6 @@
|
||||
"version": "2.0.4",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
|
||||
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -8487,12 +8307,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kubernetes-types": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lazy-val": {
|
||||
"version": "1.0.5",
|
||||
"dev": true,
|
||||
@@ -8956,43 +8770,6 @@
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
|
||||
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multipasta": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
|
||||
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"dev": true,
|
||||
@@ -9068,21 +8845,6 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"dev": true,
|
||||
@@ -9689,22 +9451,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
|
||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"license": "MIT",
|
||||
@@ -11477,15 +11223,6 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
|
||||
@@ -11951,19 +11688,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"dev": true,
|
||||
@@ -13463,7 +13187,44 @@
|
||||
"version": "0.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.14.19"
|
||||
"@opencode-ai/plugin": "1.3.7"
|
||||
}
|
||||
},
|
||||
"packages/opencode-config/node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.7.tgz",
|
||||
"integrity": "sha512-pVBIcYtHiniQ93Gj/KRkhrIz1oIAwGRifb7+dfGWdHRy00gr9DyEHFYmgHcBYgfrBavZrWw2xmqEDJdjdBuC7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.3.7",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.92",
|
||||
"@opentui/solid": ">=0.1.92"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/opencode-config/node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.7.tgz",
|
||||
"integrity": "sha512-ugkta0v0dMZchN15QGmqHb9zf35k+K1VM9wt3x4ZRJ6GxKAs0XlCmQPQJflgV9YSedNxjkgTud0GCCIWUSiUOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/opencode-config/node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
|
||||
"integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"packages/server": {
|
||||
|
||||
@@ -9,6 +9,7 @@ interface MonacoFileViewerProps {
|
||||
scopeKey: string
|
||||
path: string
|
||||
content: string
|
||||
wordWrap?: "on" | "off"
|
||||
onSave?: (content: string) => void
|
||||
onContentChange?: (content: string) => void
|
||||
}
|
||||
@@ -84,6 +85,11 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready() || !editor) return
|
||||
editor.updateOptions({ wordWrap: props.wordWrap === "on" ? "on" : "off" })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready() || !monaco || !editor) return
|
||||
const languageId = inferMonacoLanguageId(monaco, props.path)
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY,
|
||||
RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY,
|
||||
RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY,
|
||||
RIGHT_PANEL_FILES_WORD_WRAP_KEY,
|
||||
RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY,
|
||||
RIGHT_PANEL_FILES_LIST_OPEN_NONPHONE_KEY,
|
||||
RIGHT_PANEL_FILES_LIST_OPEN_PHONE_KEY,
|
||||
@@ -131,6 +132,9 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
const [diffWordWrapMode, setDiffWordWrapMode] = createSignal<DiffWordWrapMode>(
|
||||
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, ["on", "off"] as const) ?? "on",
|
||||
)
|
||||
const [filesWordWrapMode, setFilesWordWrapMode] = createSignal<DiffWordWrapMode>(
|
||||
readStoredEnum(RIGHT_PANEL_FILES_WORD_WRAP_KEY, ["on", "off"] as const) ?? "off",
|
||||
)
|
||||
|
||||
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
||||
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
||||
@@ -254,6 +258,11 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, diffWordWrapMode())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(RIGHT_PANEL_FILES_WORD_WRAP_KEY, filesWordWrapMode())
|
||||
})
|
||||
|
||||
const clampSplitWidth = (value: number) => {
|
||||
const min = 200
|
||||
const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65))
|
||||
@@ -912,6 +921,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
browserSelectedError={browserSelectedError}
|
||||
browserSelectedDirty={browserSelectedDirty}
|
||||
browserSelectedSaving={browserSelectedSaving}
|
||||
wordWrapMode={filesWordWrapMode}
|
||||
parentPath={browserParentPath}
|
||||
scopeKey={browserScopeKey}
|
||||
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
||||
@@ -919,6 +929,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
onRefresh={() => void refreshFilesTab()}
|
||||
onSave={(content: string) => void saveBrowserFile(content)}
|
||||
onContentChange={(content: string) => handleBrowserFileChange(content)}
|
||||
onWordWrapModeChange={setFilesWordWrapMode}
|
||||
listOpen={filesListOpen}
|
||||
onToggleList={toggleFilesList}
|
||||
splitWidth={filesSplitWidth}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { For, Show, Suspense, createEffect, createMemo, createSignal, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
import { Copy, RefreshCw, Save, Search } from "lucide-solid"
|
||||
import { Copy, RefreshCw, Save, Search, WrapText } from "lucide-solid"
|
||||
|
||||
import SplitFilePanel from "../components/SplitFilePanel"
|
||||
import { Markdown } from "../../../../markdown"
|
||||
import { copyToClipboard } from "../../../../../lib/clipboard"
|
||||
import { showToastNotification } from "../../../../../lib/notifications"
|
||||
import { useTheme } from "../../../../../lib/theme"
|
||||
|
||||
const LazyMonacoFileViewer = lazy(() =>
|
||||
import("../../../../file-viewer/monaco-file-viewer").then((module) => ({ default: module.MonacoFileViewer })),
|
||||
)
|
||||
|
||||
function isMarkdownPath(path: string | null | undefined): boolean {
|
||||
if (!path) return false
|
||||
return /\.(md|markdown|mdown|mkdn)$/i.test(path)
|
||||
}
|
||||
|
||||
interface FilesTabProps {
|
||||
t: (key: string, vars?: Record<string, any>) => string
|
||||
|
||||
@@ -25,6 +32,7 @@ interface FilesTabProps {
|
||||
browserSelectedError: Accessor<string | null>
|
||||
browserSelectedDirty: Accessor<boolean>
|
||||
browserSelectedSaving: Accessor<boolean>
|
||||
wordWrapMode: Accessor<"on" | "off">
|
||||
|
||||
parentPath: Accessor<string | null>
|
||||
scopeKey: Accessor<string>
|
||||
@@ -34,6 +42,7 @@ interface FilesTabProps {
|
||||
onRefresh: () => void
|
||||
onSave: (content: string) => void
|
||||
onContentChange: (content: string) => void
|
||||
onWordWrapModeChange: (mode: "on" | "off") => void
|
||||
|
||||
listOpen: Accessor<boolean>
|
||||
onToggleList: () => void
|
||||
@@ -45,6 +54,9 @@ interface FilesTabProps {
|
||||
|
||||
const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
const [filterQuery, setFilterQuery] = createSignal("")
|
||||
const { isDark } = useTheme()
|
||||
const [markdownPreviewEnabled, setMarkdownPreviewEnabled] = createSignal(false)
|
||||
let markdownPreviewRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
props.browserPath()
|
||||
@@ -78,6 +90,14 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
const listEmptyMessage = () =>
|
||||
normalizedQuery() ? props.t("instanceShell.filesShell.search.empty") : props.t("instanceShell.filesShell.listEmpty")
|
||||
|
||||
const selectedMarkdownFile = createMemo(() => isMarkdownPath(props.browserSelectedPath()))
|
||||
const showingMarkdownPreview = createMemo(() => selectedMarkdownFile() && markdownPreviewEnabled())
|
||||
|
||||
createEffect(() => {
|
||||
if (!selectedMarkdownFile()) {
|
||||
setMarkdownPreviewEnabled(false)
|
||||
}
|
||||
})
|
||||
const handleSave = () => {
|
||||
const content = props.browserSelectedContent()
|
||||
if (content !== undefined && content !== null) {
|
||||
@@ -94,6 +114,11 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!showingMarkdownPreview()) return
|
||||
requestAnimationFrame(() => markdownPreviewRef?.focus())
|
||||
})
|
||||
|
||||
const FileList: Component = () => (
|
||||
<>
|
||||
<div class="px-2 py-2 border-b border-base">
|
||||
@@ -182,6 +207,13 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
</>
|
||||
)
|
||||
|
||||
const handleMarkdownPreviewKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "s") return
|
||||
if (props.browserSelectedSaving() || !props.browserSelectedDirty()) return
|
||||
event.preventDefault()
|
||||
handleSave()
|
||||
}
|
||||
|
||||
const renderContent = (): JSX.Element => {
|
||||
const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath()
|
||||
|
||||
@@ -192,7 +224,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
|
||||
const renderViewer = () => (
|
||||
<div class="file-viewer-panel flex-1">
|
||||
<div class="file-viewer-content file-viewer-content--monaco">
|
||||
<div class={showingMarkdownPreview() ? "file-viewer-content" : "file-viewer-content file-viewer-content--monaco"}>
|
||||
<Show
|
||||
when={props.browserSelectedLoading()}
|
||||
fallback={
|
||||
@@ -212,21 +244,37 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
}
|
||||
>
|
||||
{(payload) => (
|
||||
<Suspense
|
||||
<Show
|
||||
when={showingMarkdownPreview()}
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||
</div>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LazyMonacoFileViewer
|
||||
scopeKey={props.scopeKey()}
|
||||
path={payload().path}
|
||||
content={payload().content}
|
||||
wordWrap={props.wordWrapMode()}
|
||||
onSave={props.onSave}
|
||||
onContentChange={props.onContentChange}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
<LazyMonacoFileViewer
|
||||
scopeKey={props.scopeKey()}
|
||||
path={payload().path}
|
||||
content={payload().content}
|
||||
onSave={props.onSave}
|
||||
onContentChange={props.onContentChange}
|
||||
/>
|
||||
</Suspense>
|
||||
<div
|
||||
ref={markdownPreviewRef}
|
||||
class="h-full outline-none"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMarkdownPreviewKeyDown}
|
||||
onMouseDown={() => markdownPreviewRef?.focus()}
|
||||
>
|
||||
<Markdown part={{ type: "text", text: payload().content }} isDark={isDark()} escapeRawHtml />
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
}
|
||||
@@ -262,13 +310,33 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class={`file-viewer-toolbar-button${showingMarkdownPreview() ? " active" : ""}`}
|
||||
disabled={!selectedMarkdownFile()}
|
||||
style={{ "margin-inline-start": "auto" }}
|
||||
onClick={() => selectedMarkdownFile() && setMarkdownPreviewEnabled((prev) => !prev)}
|
||||
>
|
||||
{showingMarkdownPreview()
|
||||
? props.t("instanceShell.filesShell.showSource")
|
||||
: props.t("instanceShell.filesShell.previewMarkdown")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`file-viewer-toolbar-icon-button${props.wordWrapMode() === "on" ? " active" : ""}`}
|
||||
title={props.wordWrapMode() === "on" ? props.t("instanceShell.filesShell.disableWordWrap") : props.t("instanceShell.filesShell.enableWordWrap")}
|
||||
aria-label={props.wordWrapMode() === "on" ? props.t("instanceShell.filesShell.disableWordWrap") : props.t("instanceShell.filesShell.enableWordWrap")}
|
||||
disabled={showingMarkdownPreview()}
|
||||
onClick={() => props.onWordWrapModeChange(props.wordWrapMode() === "on" ? "off" : "on")}
|
||||
>
|
||||
<WrapText class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="files-header-icon-button"
|
||||
title={props.t("instanceShell.rightPanel.actions.save") || "Save (Ctrl+S)"}
|
||||
aria-label={props.t("instanceShell.rightPanel.actions.save") || "Save"}
|
||||
disabled={props.browserSelectedSaving() || !props.browserSelectedDirty()}
|
||||
style={{ "margin-inline-start": "auto" }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Show when={props.browserSelectedSaving()} fallback={<Save class="h-4 w-4" />}>
|
||||
|
||||
@@ -28,6 +28,7 @@ export const RIGHT_PANEL_GIT_CHANGES_UNSTAGED_OPEN_PHONE_KEY = "opencode-session
|
||||
export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
||||
export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
||||
export const RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY = "opencode-session-right-panel-changes-diff-word-wrap-v1"
|
||||
export const RIGHT_PANEL_FILES_WORD_WRAP_KEY = "opencode-session-right-panel-files-word-wrap-v1"
|
||||
|
||||
export const clampWidth = (value: number) =>
|
||||
Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||
|
||||
@@ -158,6 +158,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "Copy path",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "Copied path",
|
||||
"instanceShell.filesShell.toast.copyPathError": "Failed to copy path",
|
||||
"instanceShell.filesShell.previewMarkdown": "Preview Markdown",
|
||||
"instanceShell.filesShell.showSource": "Show source",
|
||||
"instanceShell.filesShell.enableWordWrap": "Enable word wrap",
|
||||
"instanceShell.filesShell.disableWordWrap": "Disable word wrap",
|
||||
"instanceShell.diff.hideUnchanged": "Hide unchanged regions",
|
||||
"instanceShell.diff.showFull": "Show full file",
|
||||
"instanceShell.diff.switchToSplit": "Switch to split view",
|
||||
|
||||
@@ -155,6 +155,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "Copiar ruta",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "Ruta copiada",
|
||||
"instanceShell.filesShell.toast.copyPathError": "No se pudo copiar la ruta",
|
||||
"instanceShell.filesShell.previewMarkdown": "Vista previa Markdown",
|
||||
"instanceShell.filesShell.showSource": "Mostrar fuente",
|
||||
"instanceShell.filesShell.enableWordWrap": "Activar ajuste de línea",
|
||||
"instanceShell.filesShell.disableWordWrap": "Desactivar ajuste de línea",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
||||
|
||||
@@ -155,6 +155,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "Copier le chemin",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "Chemin copié",
|
||||
"instanceShell.filesShell.toast.copyPathError": "Impossible de copier le chemin",
|
||||
"instanceShell.filesShell.previewMarkdown": "Aperçu Markdown",
|
||||
"instanceShell.filesShell.showSource": "Afficher la source",
|
||||
"instanceShell.filesShell.enableWordWrap": "Activer le retour à la ligne",
|
||||
"instanceShell.filesShell.disableWordWrap": "Désactiver le retour à la ligne",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
||||
|
||||
@@ -142,6 +142,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "העתק נתיב",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "הנתיב הועתק",
|
||||
"instanceShell.filesShell.toast.copyPathError": "העתקת הנתיב נכשלה",
|
||||
"instanceShell.filesShell.previewMarkdown": "תצוגת Markdown",
|
||||
"instanceShell.filesShell.showSource": "הצג מקור",
|
||||
"instanceShell.filesShell.enableWordWrap": "הפעל גלישת מילים",
|
||||
"instanceShell.filesShell.disableWordWrap": "כבה גלישת מילים",
|
||||
"instanceShell.gitChanges.noSessionSelected": "בחר סשן לצפייה בשינויי Git.",
|
||||
"instanceShell.gitChanges.loading": "טוען שינויי Git…",
|
||||
"instanceShell.gitChanges.empty": "אין שינויי Git עדיין.",
|
||||
|
||||
@@ -155,6 +155,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "パスをコピー",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "パスをコピーしました",
|
||||
"instanceShell.filesShell.toast.copyPathError": "パスをコピーできませんでした",
|
||||
"instanceShell.filesShell.previewMarkdown": "Markdown プレビュー",
|
||||
"instanceShell.filesShell.showSource": "ソースを表示",
|
||||
"instanceShell.filesShell.enableWordWrap": "折り返しを有効化",
|
||||
"instanceShell.filesShell.disableWordWrap": "折り返しを無効化",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
||||
|
||||
@@ -155,6 +155,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "Скопировать путь",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "Путь скопирован",
|
||||
"instanceShell.filesShell.toast.copyPathError": "Не удалось скопировать путь",
|
||||
"instanceShell.filesShell.previewMarkdown": "Предпросмотр Markdown",
|
||||
"instanceShell.filesShell.showSource": "Показать исходник",
|
||||
"instanceShell.filesShell.enableWordWrap": "Включить перенос строк",
|
||||
"instanceShell.filesShell.disableWordWrap": "Отключить перенос строк",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
||||
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
||||
|
||||
@@ -155,6 +155,10 @@ export const instanceMessages = {
|
||||
"instanceShell.filesShell.actions.copyPath": "复制路径",
|
||||
"instanceShell.filesShell.toast.copyPathSuccess": "路径已复制",
|
||||
"instanceShell.filesShell.toast.copyPathError": "无法复制路径",
|
||||
"instanceShell.filesShell.previewMarkdown": "Markdown 预览",
|
||||
"instanceShell.filesShell.showSource": "显示源码",
|
||||
"instanceShell.filesShell.enableWordWrap": "启用自动换行",
|
||||
"instanceShell.filesShell.disableWordWrap": "禁用自动换行",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
||||
"instanceShell.plan.empty": "暂无计划。",
|
||||
|
||||
@@ -16,6 +16,135 @@ let rendererSetup = false
|
||||
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
||||
|
||||
const ALLOWED_RAW_HTML_TAGS = new Set([
|
||||
"a",
|
||||
"blockquote",
|
||||
"br",
|
||||
"code",
|
||||
"del",
|
||||
"details",
|
||||
"div",
|
||||
"em",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hr",
|
||||
"img",
|
||||
"kbd",
|
||||
"li",
|
||||
"ol",
|
||||
"p",
|
||||
"pre",
|
||||
"span",
|
||||
"strong",
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"th",
|
||||
"thead",
|
||||
"tr",
|
||||
"ul",
|
||||
])
|
||||
|
||||
const DROP_RAW_HTML_TAGS = new Set(["script", "style", "iframe", "object", "embed", "meta", "link"])
|
||||
|
||||
function sanitizeUrlAttribute(tagName: string, attrName: string, value: string): string | null {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return null
|
||||
|
||||
if (attrName === "src" && tagName === "img") {
|
||||
if (/^(https?:|data:image\/|\/|\.\/|\.\.\/|#)/i.test(trimmed)) return trimmed
|
||||
return null
|
||||
}
|
||||
|
||||
if (attrName === "href" && tagName === "a") {
|
||||
if (/^(https?:|mailto:|\/|\.\/|\.\.\/|#)/i.test(trimmed)) return trimmed
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function sanitizeRawHtmlFragment(html: string): string {
|
||||
const decoded = decodeHtmlEntities(html)
|
||||
if (typeof document === "undefined") {
|
||||
return escapeHtml(decoded)
|
||||
}
|
||||
|
||||
const template = document.createElement("template")
|
||||
template.innerHTML = decoded
|
||||
|
||||
const sanitizeElement = (element: Element) => {
|
||||
const tagName = element.tagName.toLowerCase()
|
||||
if (DROP_RAW_HTML_TAGS.has(tagName)) {
|
||||
element.remove()
|
||||
return
|
||||
}
|
||||
|
||||
if (!ALLOWED_RAW_HTML_TAGS.has(tagName)) {
|
||||
element.replaceWith(...Array.from(element.childNodes))
|
||||
return
|
||||
}
|
||||
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
const attrName = attr.name.toLowerCase()
|
||||
if (attrName.startsWith("on") || attrName === "style") {
|
||||
element.removeAttribute(attr.name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (attrName === "href" || attrName === "src") {
|
||||
const sanitized = sanitizeUrlAttribute(tagName, attrName, attr.value)
|
||||
if (sanitized) {
|
||||
element.setAttribute(attr.name, sanitized)
|
||||
continue
|
||||
}
|
||||
element.removeAttribute(attr.name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
attrName === "alt" ||
|
||||
attrName === "title" ||
|
||||
attrName === "width" ||
|
||||
attrName === "height" ||
|
||||
attrName === "open" ||
|
||||
attrName === "id" ||
|
||||
attrName === "class" ||
|
||||
attrName === "name" ||
|
||||
attrName.startsWith("aria-") ||
|
||||
attrName.startsWith("data-")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
element.removeAttribute(attr.name)
|
||||
}
|
||||
|
||||
if (tagName === "a") {
|
||||
element.setAttribute("target", "_blank")
|
||||
element.setAttribute("rel", "noopener noreferrer")
|
||||
}
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT)
|
||||
const elements: Element[] = []
|
||||
while (walker.nextNode()) {
|
||||
elements.push(walker.currentNode as Element)
|
||||
}
|
||||
for (const element of elements.reverse()) {
|
||||
sanitizeElement(element)
|
||||
}
|
||||
|
||||
return template.innerHTML
|
||||
}
|
||||
|
||||
// Track loaded languages and queue for on-demand loading
|
||||
const loadedLanguages = new Set<string>()
|
||||
const queuedLanguages = new Set<string>()
|
||||
@@ -318,7 +447,7 @@ function setupRenderer(isDark: boolean) {
|
||||
return html
|
||||
}
|
||||
|
||||
return escapeHtml(decodeHtmlEntities(html))
|
||||
return sanitizeRawHtmlFragment(html)
|
||||
}
|
||||
|
||||
marked.use({ renderer })
|
||||
|
||||
Reference in New Issue
Block a user