victor HF Staff commited on
Commit
97bf9da
·
unverified ·
1 Parent(s): 7b56bb5

fix(markdown): use htmlparser2 instead of DOMPurify for Web Worker compatibility (#2039)

Browse files

- Replace isomorphic-dompurify with htmlparser2 (works in Web Workers)
- Add sanitizeHtmlForMultimedia() for video/audio/source tag allowlisting
- Update tests to expect escaped (not stripped) disallowed tags

package-lock.json CHANGED
@@ -24,6 +24,7 @@
24
  "file-type": "^21.0.0",
25
  "handlebars": "^4.7.8",
26
  "highlight.js": "^11.7.0",
 
27
  "husky": "^9.0.11",
28
  "ip-address": "^9.0.5",
29
  "jsdom": "^22.0.0",
@@ -680,6 +681,7 @@
680
  "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.927.0.tgz",
681
  "integrity": "sha512-CasoHKKE/K+6YcVqjE+v5dVyKqKBtfzZyvGi669HvJ1f4EPHbVRPPLIb0eAYd/aEmwHsB/nn9VnyN9Wq5OppUQ==",
682
  "license": "Apache-2.0",
 
683
  "dependencies": {
684
  "@aws-sdk/client-cognito-identity": "3.927.0",
685
  "@aws-sdk/core": "3.927.0",
@@ -956,7 +958,6 @@
956
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
957
  "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
958
  "license": "MIT",
959
- "peer": true,
960
  "dependencies": {
961
  "@babel/helper-validator-identifier": "^7.27.1",
962
  "js-tokens": "^4.0.0",
@@ -971,7 +972,6 @@
971
  "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
972
  "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
973
  "license": "MIT",
974
- "peer": true,
975
  "engines": {
976
  "node": ">=6.9.0"
977
  }
@@ -1073,6 +1073,7 @@
1073
  }
1074
  ],
1075
  "license": "MIT",
 
1076
  "engines": {
1077
  "node": ">=18"
1078
  },
@@ -1096,6 +1097,7 @@
1096
  }
1097
  ],
1098
  "license": "MIT",
 
1099
  "engines": {
1100
  "node": ">=18"
1101
  }
@@ -3248,7 +3250,8 @@
3248
  "version": "0.34.41",
3249
  "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
3250
  "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
3251
- "license": "MIT"
 
3252
  },
3253
  "node_modules/@smithy/abort-controller": {
3254
  "version": "4.2.4",
@@ -3861,6 +3864,7 @@
3861
  "integrity": "sha512-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA==",
3862
  "devOptional": true,
3863
  "license": "MIT",
 
3864
  "dependencies": {
3865
  "@sveltejs/acorn-typescript": "^1.0.5",
3866
  "@types/cookie": "^0.6.0",
@@ -3893,6 +3897,7 @@
3893
  "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==",
3894
  "devOptional": true,
3895
  "license": "MIT",
 
3896
  "dependencies": {
3897
  "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
3898
  "debug": "^4.4.1",
@@ -3932,7 +3937,6 @@
3932
  "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
3933
  "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
3934
  "license": "Apache-2.0",
3935
- "peer": true,
3936
  "dependencies": {
3937
  "tslib": "^2.8.0"
3938
  }
@@ -3978,7 +3982,6 @@
3978
  "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
3979
  "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
3980
  "license": "MIT",
3981
- "peer": true,
3982
  "engines": {
3983
  "node": ">=12",
3984
  "npm": ">=6"
@@ -4024,8 +4027,7 @@
4024
  "version": "5.0.4",
4025
  "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
4026
  "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
4027
- "license": "MIT",
4028
- "peer": true
4029
  },
4030
  "node_modules/@types/chai": {
4031
  "version": "5.2.2",
@@ -4254,6 +4256,7 @@
4254
  "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
4255
  "dev": true,
4256
  "license": "BSD-2-Clause",
 
4257
  "dependencies": {
4258
  "@typescript-eslint/scope-manager": "6.21.0",
4259
  "@typescript-eslint/types": "6.21.0",
@@ -4652,6 +4655,7 @@
4652
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
4653
  "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
4654
  "license": "MIT",
 
4655
  "bin": {
4656
  "acorn": "bin/acorn"
4657
  },
@@ -4835,7 +4839,6 @@
4835
  "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
4836
  "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
4837
  "license": "Apache-2.0",
4838
- "peer": true,
4839
  "dependencies": {
4840
  "dequal": "^2.0.3"
4841
  }
@@ -5101,6 +5104,7 @@
5101
  }
5102
  ],
5103
  "license": "MIT",
 
5104
  "dependencies": {
5105
  "caniuse-lite": "^1.0.30001718",
5106
  "electron-to-chromium": "^1.5.160",
@@ -5770,8 +5774,45 @@
5770
  "version": "0.5.16",
5771
  "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
5772
  "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
 
 
 
 
 
 
5773
  "license": "MIT",
5774
- "peer": true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5775
  },
5776
  "node_modules/domexception": {
5777
  "version": "4.0.0",
@@ -5786,6 +5827,21 @@
5786
  "node": ">=12"
5787
  }
5788
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5789
  "node_modules/dompurify": {
5790
  "version": "3.3.0",
5791
  "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
@@ -5796,6 +5852,20 @@
5796
  "@types/trusted-types": "^2.0.7"
5797
  }
5798
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5799
  "node_modules/dotenv": {
5800
  "version": "16.5.0",
5801
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
@@ -5845,6 +5915,7 @@
5845
  "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.3.4.tgz",
5846
  "integrity": "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A==",
5847
  "license": "MIT",
 
5848
  "dependencies": {
5849
  "cookie": "^1.0.2",
5850
  "exact-mirror": "0.1.2",
@@ -6045,6 +6116,7 @@
6045
  "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
6046
  "dev": true,
6047
  "license": "MIT",
 
6048
  "dependencies": {
6049
  "@eslint-community/eslint-utils": "^4.2.0",
6050
  "@eslint-community/regexpp": "^4.6.1",
@@ -6682,6 +6754,7 @@
6682
  "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
6683
  "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
6684
  "license": "MIT",
 
6685
  "dependencies": {
6686
  "@tokenizer/inflate": "^0.2.7",
6687
  "strtok3": "^10.2.2",
@@ -7213,6 +7286,25 @@
7213
  "node": ">=12"
7214
  }
7215
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7216
  "node_modules/http-errors": {
7217
  "version": "2.0.0",
7218
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -7822,8 +7914,7 @@
7822
  "version": "4.0.0",
7823
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
7824
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
7825
- "license": "MIT",
7826
- "peer": true
7827
  },
7828
  "node_modules/js-yaml": {
7829
  "version": "4.1.0",
@@ -9601,6 +9692,7 @@
9601
  "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
9602
  "devOptional": true,
9603
  "license": "Apache-2.0",
 
9604
  "dependencies": {
9605
  "playwright-core": "1.55.1"
9606
  },
@@ -9646,6 +9738,7 @@
9646
  }
9647
  ],
9648
  "license": "MIT",
 
9649
  "dependencies": {
9650
  "nanoid": "^3.3.11",
9651
  "picocolors": "^1.1.1",
@@ -9877,6 +9970,7 @@
9877
  "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
9878
  "dev": true,
9879
  "license": "MIT",
 
9880
  "bin": {
9881
  "prettier": "bin/prettier.cjs"
9882
  },
@@ -9893,6 +9987,7 @@
9893
  "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
9894
  "dev": true,
9895
  "license": "MIT",
 
9896
  "peerDependencies": {
9897
  "prettier": "^3.0.0",
9898
  "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
@@ -9982,7 +10077,6 @@
9982
  "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
9983
  "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
9984
  "license": "MIT",
9985
- "peer": true,
9986
  "dependencies": {
9987
  "ansi-regex": "^5.0.1",
9988
  "ansi-styles": "^5.0.0",
@@ -9997,7 +10091,6 @@
9997
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
9998
  "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
9999
  "license": "MIT",
10000
- "peer": true,
10001
  "engines": {
10002
  "node": ">=10"
10003
  },
@@ -10206,8 +10299,7 @@
10206
  "version": "17.0.2",
10207
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
10208
  "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
10209
- "license": "MIT",
10210
- "peer": true
10211
  },
10212
  "node_modules/read-cache": {
10213
  "version": "1.0.0",
@@ -10371,6 +10463,7 @@
10371
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
10372
  "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
10373
  "license": "MIT",
 
10374
  "dependencies": {
10375
  "@types/estree": "1.0.7"
10376
  },
@@ -11289,6 +11382,7 @@
11289
  "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.14.tgz",
11290
  "integrity": "sha512-kRlbhIlMTijbFmVDQFDeKXPLlX1/ovXwV0I162wRqQhRcygaqDIcu1d/Ese3H2uI+yt3uT8E7ndgDthQv5v5BA==",
11291
  "license": "MIT",
 
11292
  "dependencies": {
11293
  "@ampproject/remapping": "^2.3.0",
11294
  "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -11428,6 +11522,7 @@
11428
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
11429
  "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
11430
  "license": "MIT",
 
11431
  "dependencies": {
11432
  "@alloc/quick-lru": "^5.2.0",
11433
  "arg": "^5.0.2",
@@ -11885,6 +11980,7 @@
11885
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
11886
  "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
11887
  "license": "Apache-2.0",
 
11888
  "bin": {
11889
  "tsc": "bin/tsc",
11890
  "tsserver": "bin/tsserver"
@@ -12120,6 +12216,7 @@
12120
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
12121
  "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
12122
  "license": "MIT",
 
12123
  "dependencies": {
12124
  "esbuild": "^0.25.0",
12125
  "fdir": "^6.4.4",
@@ -12255,6 +12352,7 @@
12255
  "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.2.tgz",
12256
  "integrity": "sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==",
12257
  "license": "MIT",
 
12258
  "dependencies": {
12259
  "@types/chai": "^5.2.2",
12260
  "@vitest/expect": "3.2.2",
@@ -12577,6 +12675,7 @@
12577
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
12578
  "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
12579
  "license": "MIT",
 
12580
  "engines": {
12581
  "node": ">=10.0.0"
12582
  },
@@ -12699,6 +12798,7 @@
12699
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz",
12700
  "integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==",
12701
  "license": "MIT",
 
12702
  "funding": {
12703
  "url": "https://github.com/sponsors/colinhacks"
12704
  }
 
24
  "file-type": "^21.0.0",
25
  "handlebars": "^4.7.8",
26
  "highlight.js": "^11.7.0",
27
+ "htmlparser2": "^10.0.0",
28
  "husky": "^9.0.11",
29
  "ip-address": "^9.0.5",
30
  "jsdom": "^22.0.0",
 
681
  "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.927.0.tgz",
682
  "integrity": "sha512-CasoHKKE/K+6YcVqjE+v5dVyKqKBtfzZyvGi669HvJ1f4EPHbVRPPLIb0eAYd/aEmwHsB/nn9VnyN9Wq5OppUQ==",
683
  "license": "Apache-2.0",
684
+ "peer": true,
685
  "dependencies": {
686
  "@aws-sdk/client-cognito-identity": "3.927.0",
687
  "@aws-sdk/core": "3.927.0",
 
958
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
959
  "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
960
  "license": "MIT",
 
961
  "dependencies": {
962
  "@babel/helper-validator-identifier": "^7.27.1",
963
  "js-tokens": "^4.0.0",
 
972
  "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
973
  "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
974
  "license": "MIT",
 
975
  "engines": {
976
  "node": ">=6.9.0"
977
  }
 
1073
  }
1074
  ],
1075
  "license": "MIT",
1076
+ "peer": true,
1077
  "engines": {
1078
  "node": ">=18"
1079
  },
 
1097
  }
1098
  ],
1099
  "license": "MIT",
1100
+ "peer": true,
1101
  "engines": {
1102
  "node": ">=18"
1103
  }
 
3250
  "version": "0.34.41",
3251
  "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
3252
  "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
3253
+ "license": "MIT",
3254
+ "peer": true
3255
  },
3256
  "node_modules/@smithy/abort-controller": {
3257
  "version": "4.2.4",
 
3864
  "integrity": "sha512-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA==",
3865
  "devOptional": true,
3866
  "license": "MIT",
3867
+ "peer": true,
3868
  "dependencies": {
3869
  "@sveltejs/acorn-typescript": "^1.0.5",
3870
  "@types/cookie": "^0.6.0",
 
3897
  "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==",
3898
  "devOptional": true,
3899
  "license": "MIT",
3900
+ "peer": true,
3901
  "dependencies": {
3902
  "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
3903
  "debug": "^4.4.1",
 
3937
  "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
3938
  "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
3939
  "license": "Apache-2.0",
 
3940
  "dependencies": {
3941
  "tslib": "^2.8.0"
3942
  }
 
3982
  "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
3983
  "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
3984
  "license": "MIT",
 
3985
  "engines": {
3986
  "node": ">=12",
3987
  "npm": ">=6"
 
4027
  "version": "5.0.4",
4028
  "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
4029
  "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
4030
+ "license": "MIT"
 
4031
  },
4032
  "node_modules/@types/chai": {
4033
  "version": "5.2.2",
 
4256
  "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
4257
  "dev": true,
4258
  "license": "BSD-2-Clause",
4259
+ "peer": true,
4260
  "dependencies": {
4261
  "@typescript-eslint/scope-manager": "6.21.0",
4262
  "@typescript-eslint/types": "6.21.0",
 
4655
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
4656
  "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
4657
  "license": "MIT",
4658
+ "peer": true,
4659
  "bin": {
4660
  "acorn": "bin/acorn"
4661
  },
 
4839
  "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
4840
  "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
4841
  "license": "Apache-2.0",
 
4842
  "dependencies": {
4843
  "dequal": "^2.0.3"
4844
  }
 
5104
  }
5105
  ],
5106
  "license": "MIT",
5107
+ "peer": true,
5108
  "dependencies": {
5109
  "caniuse-lite": "^1.0.30001718",
5110
  "electron-to-chromium": "^1.5.160",
 
5774
  "version": "0.5.16",
5775
  "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
5776
  "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
5777
+ "license": "MIT"
5778
+ },
5779
+ "node_modules/dom-serializer": {
5780
+ "version": "2.0.0",
5781
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
5782
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
5783
  "license": "MIT",
5784
+ "dependencies": {
5785
+ "domelementtype": "^2.3.0",
5786
+ "domhandler": "^5.0.2",
5787
+ "entities": "^4.2.0"
5788
+ },
5789
+ "funding": {
5790
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
5791
+ }
5792
+ },
5793
+ "node_modules/dom-serializer/node_modules/entities": {
5794
+ "version": "4.5.0",
5795
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
5796
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
5797
+ "license": "BSD-2-Clause",
5798
+ "engines": {
5799
+ "node": ">=0.12"
5800
+ },
5801
+ "funding": {
5802
+ "url": "https://github.com/fb55/entities?sponsor=1"
5803
+ }
5804
+ },
5805
+ "node_modules/domelementtype": {
5806
+ "version": "2.3.0",
5807
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
5808
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
5809
+ "funding": [
5810
+ {
5811
+ "type": "github",
5812
+ "url": "https://github.com/sponsors/fb55"
5813
+ }
5814
+ ],
5815
+ "license": "BSD-2-Clause"
5816
  },
5817
  "node_modules/domexception": {
5818
  "version": "4.0.0",
 
5827
  "node": ">=12"
5828
  }
5829
  },
5830
+ "node_modules/domhandler": {
5831
+ "version": "5.0.3",
5832
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
5833
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
5834
+ "license": "BSD-2-Clause",
5835
+ "dependencies": {
5836
+ "domelementtype": "^2.3.0"
5837
+ },
5838
+ "engines": {
5839
+ "node": ">= 4"
5840
+ },
5841
+ "funding": {
5842
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
5843
+ }
5844
+ },
5845
  "node_modules/dompurify": {
5846
  "version": "3.3.0",
5847
  "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
 
5852
  "@types/trusted-types": "^2.0.7"
5853
  }
5854
  },
5855
+ "node_modules/domutils": {
5856
+ "version": "3.2.2",
5857
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
5858
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
5859
+ "license": "BSD-2-Clause",
5860
+ "dependencies": {
5861
+ "dom-serializer": "^2.0.0",
5862
+ "domelementtype": "^2.3.0",
5863
+ "domhandler": "^5.0.3"
5864
+ },
5865
+ "funding": {
5866
+ "url": "https://github.com/fb55/domutils?sponsor=1"
5867
+ }
5868
+ },
5869
  "node_modules/dotenv": {
5870
  "version": "16.5.0",
5871
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
 
5915
  "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.3.4.tgz",
5916
  "integrity": "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A==",
5917
  "license": "MIT",
5918
+ "peer": true,
5919
  "dependencies": {
5920
  "cookie": "^1.0.2",
5921
  "exact-mirror": "0.1.2",
 
6116
  "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
6117
  "dev": true,
6118
  "license": "MIT",
6119
+ "peer": true,
6120
  "dependencies": {
6121
  "@eslint-community/eslint-utils": "^4.2.0",
6122
  "@eslint-community/regexpp": "^4.6.1",
 
6754
  "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
6755
  "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
6756
  "license": "MIT",
6757
+ "peer": true,
6758
  "dependencies": {
6759
  "@tokenizer/inflate": "^0.2.7",
6760
  "strtok3": "^10.2.2",
 
7286
  "node": ">=12"
7287
  }
7288
  },
7289
+ "node_modules/htmlparser2": {
7290
+ "version": "10.0.0",
7291
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
7292
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
7293
+ "funding": [
7294
+ "https://github.com/fb55/htmlparser2?sponsor=1",
7295
+ {
7296
+ "type": "github",
7297
+ "url": "https://github.com/sponsors/fb55"
7298
+ }
7299
+ ],
7300
+ "license": "MIT",
7301
+ "dependencies": {
7302
+ "domelementtype": "^2.3.0",
7303
+ "domhandler": "^5.0.3",
7304
+ "domutils": "^3.2.1",
7305
+ "entities": "^6.0.0"
7306
+ }
7307
+ },
7308
  "node_modules/http-errors": {
7309
  "version": "2.0.0",
7310
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
 
7914
  "version": "4.0.0",
7915
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
7916
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
7917
+ "license": "MIT"
 
7918
  },
7919
  "node_modules/js-yaml": {
7920
  "version": "4.1.0",
 
9692
  "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
9693
  "devOptional": true,
9694
  "license": "Apache-2.0",
9695
+ "peer": true,
9696
  "dependencies": {
9697
  "playwright-core": "1.55.1"
9698
  },
 
9738
  }
9739
  ],
9740
  "license": "MIT",
9741
+ "peer": true,
9742
  "dependencies": {
9743
  "nanoid": "^3.3.11",
9744
  "picocolors": "^1.1.1",
 
9970
  "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
9971
  "dev": true,
9972
  "license": "MIT",
9973
+ "peer": true,
9974
  "bin": {
9975
  "prettier": "bin/prettier.cjs"
9976
  },
 
9987
  "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
9988
  "dev": true,
9989
  "license": "MIT",
9990
+ "peer": true,
9991
  "peerDependencies": {
9992
  "prettier": "^3.0.0",
9993
  "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
 
10077
  "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
10078
  "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
10079
  "license": "MIT",
 
10080
  "dependencies": {
10081
  "ansi-regex": "^5.0.1",
10082
  "ansi-styles": "^5.0.0",
 
10091
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
10092
  "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
10093
  "license": "MIT",
 
10094
  "engines": {
10095
  "node": ">=10"
10096
  },
 
10299
  "version": "17.0.2",
10300
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
10301
  "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
10302
+ "license": "MIT"
 
10303
  },
10304
  "node_modules/read-cache": {
10305
  "version": "1.0.0",
 
10463
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
10464
  "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
10465
  "license": "MIT",
10466
+ "peer": true,
10467
  "dependencies": {
10468
  "@types/estree": "1.0.7"
10469
  },
 
11382
  "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.14.tgz",
11383
  "integrity": "sha512-kRlbhIlMTijbFmVDQFDeKXPLlX1/ovXwV0I162wRqQhRcygaqDIcu1d/Ese3H2uI+yt3uT8E7ndgDthQv5v5BA==",
11384
  "license": "MIT",
11385
+ "peer": true,
11386
  "dependencies": {
11387
  "@ampproject/remapping": "^2.3.0",
11388
  "@jridgewell/sourcemap-codec": "^1.5.0",
 
11522
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
11523
  "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
11524
  "license": "MIT",
11525
+ "peer": true,
11526
  "dependencies": {
11527
  "@alloc/quick-lru": "^5.2.0",
11528
  "arg": "^5.0.2",
 
11980
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
11981
  "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
11982
  "license": "Apache-2.0",
11983
+ "peer": true,
11984
  "bin": {
11985
  "tsc": "bin/tsc",
11986
  "tsserver": "bin/tsserver"
 
12216
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
12217
  "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
12218
  "license": "MIT",
12219
+ "peer": true,
12220
  "dependencies": {
12221
  "esbuild": "^0.25.0",
12222
  "fdir": "^6.4.4",
 
12352
  "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.2.tgz",
12353
  "integrity": "sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==",
12354
  "license": "MIT",
12355
+ "peer": true,
12356
  "dependencies": {
12357
  "@types/chai": "^5.2.2",
12358
  "@vitest/expect": "3.2.2",
 
12675
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
12676
  "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
12677
  "license": "MIT",
12678
+ "peer": true,
12679
  "engines": {
12680
  "node": ">=10.0.0"
12681
  },
 
12798
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz",
12799
  "integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==",
12800
  "license": "MIT",
12801
+ "peer": true,
12802
  "funding": {
12803
  "url": "https://github.com/sponsors/colinhacks"
12804
  }
package.json CHANGED
@@ -83,6 +83,7 @@
83
  "file-type": "^21.0.0",
84
  "handlebars": "^4.7.8",
85
  "highlight.js": "^11.7.0",
 
86
  "husky": "^9.0.11",
87
  "ip-address": "^9.0.5",
88
  "jsdom": "^22.0.0",
 
83
  "file-type": "^21.0.0",
84
  "handlebars": "^4.7.8",
85
  "highlight.js": "^11.7.0",
86
+ "htmlparser2": "^10.0.0",
87
  "husky": "^9.0.11",
88
  "ip-address": "^9.0.5",
89
  "jsdom": "^22.0.0",
src/lib/components/chat/MarkdownRenderer.svelte.test.ts CHANGED
@@ -32,8 +32,8 @@ describe("MarkdownRenderer", () => {
32
  it("doesnt render raw html directly", () => {
33
  render(MarkdownRenderer, { content: "<button>Click me</button>" });
34
  expect(page.getByRole("button").elements).toHaveLength(0);
35
- // DOMPurify strips disallowed tags but preserves text content
36
- expect(page.getByRole("paragraph")).toHaveTextContent("Click me");
37
  });
38
  it("renders latex", () => {
39
  const { baseElement } = render(MarkdownRenderer, { content: "$(oo)^2$" });
 
32
  it("doesnt render raw html directly", () => {
33
  render(MarkdownRenderer, { content: "<button>Click me</button>" });
34
  expect(page.getByRole("button").elements).toHaveLength(0);
35
+ // htmlparser2 escapes disallowed tags
36
+ expect(page.getByRole("paragraph")).toHaveTextContent("<button>Click me</button>");
37
  });
38
  it("renders latex", () => {
39
  const { baseElement } = render(MarkdownRenderer, { content: "$(oo)^2$" });
src/lib/utils/marked.spec.ts CHANGED
@@ -79,9 +79,10 @@ describe("marked html video tag support", () => {
79
  expect(html).not.toContain("javascript:");
80
  });
81
 
82
- test("strips disallowed html tags", () => {
83
  const html = renderHtml("<script>alert(1)</script>");
84
  expect(html).not.toContain("<script>");
 
85
  });
86
 
87
  test("allows <audio> tags with controls", () => {
 
79
  expect(html).not.toContain("javascript:");
80
  });
81
 
82
+ test("escapes disallowed html tags", () => {
83
  const html = renderHtml("<script>alert(1)</script>");
84
  expect(html).not.toContain("<script>");
85
+ expect(html).toContain("&lt;script&gt;");
86
  });
87
 
88
  test("allows <audio> tags with controls", () => {
src/lib/utils/marked.ts CHANGED
@@ -2,7 +2,7 @@ import katex from "katex";
2
  import "katex/dist/contrib/mhchem.mjs";
3
  import { Marked } from "marked";
4
  import type { Tokens, TokenizerExtension, RendererExtension } from "marked";
5
- import DOMPurify from "isomorphic-dompurify";
6
  // Simple type to replace removed WebSearchSource
7
  type SimpleSource = {
8
  title?: string;
@@ -57,24 +57,6 @@ const bundledLanguages: [string, LanguageFn][] = [
57
 
58
  bundledLanguages.forEach(([name, language]) => hljs.registerLanguage(name, language));
59
 
60
- // DOMPurify config for allowing video/audio tags in markdown
61
- const DOMPURIFY_CONFIG = {
62
- ALLOWED_TAGS: ["video", "audio", "source"],
63
- ALLOWED_ATTR: [
64
- "src",
65
- "type",
66
- "controls",
67
- "autoplay",
68
- "loop",
69
- "muted",
70
- "playsinline",
71
- "poster",
72
- "width",
73
- "height",
74
- "preload",
75
- ],
76
- };
77
-
78
  // Media URL detection
79
  const VIDEO_EXTENSIONS = /\.(mp4|webm|ogg|mov|m4v)([?#]|$)/i;
80
  const AUDIO_EXTENSIONS = /\.(mp3|wav|m4a|aac|flac)([?#]|$)/i;
@@ -87,6 +69,34 @@ function isAudioUrl(url: string): boolean {
87
  return AUDIO_EXTENSIONS.test(url);
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  interface katexBlockToken extends Tokens.Generic {
91
  type: "katexBlock";
92
  raw: string;
@@ -256,6 +266,82 @@ function highlightCode(text: string, lang?: string): string {
256
  return hljs.highlightAuto(text).value;
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  function createMarkedInstance(sources: SimpleSource[]): Marked {
260
  return new Marked({
261
  hooks: {
@@ -285,7 +371,7 @@ function createMarkedInstance(sources: SimpleSource[]): Marked {
285
  }
286
  return `<img src="${safeSrc}" alt="${safeAlt}"${safeTitle} />`;
287
  },
288
- html: (html) => DOMPurify.sanitize(html, DOMPURIFY_CONFIG),
289
  },
290
  gfm: true,
291
  breaks: true,
 
2
  import "katex/dist/contrib/mhchem.mjs";
3
  import { Marked } from "marked";
4
  import type { Tokens, TokenizerExtension, RendererExtension } from "marked";
5
+ import { parseDocument } from "htmlparser2";
6
  // Simple type to replace removed WebSearchSource
7
  type SimpleSource = {
8
  title?: string;
 
57
 
58
  bundledLanguages.forEach(([name, language]) => hljs.registerLanguage(name, language));
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  // Media URL detection
61
  const VIDEO_EXTENSIONS = /\.(mp4|webm|ogg|mov|m4v)([?#]|$)/i;
62
  const AUDIO_EXTENSIONS = /\.(mp3|wav|m4a|aac|flac)([?#]|$)/i;
 
69
  return AUDIO_EXTENSIONS.test(url);
70
  }
71
 
72
+ // Multimedia HTML sanitization (works in Web Workers - no DOM needed)
73
+ const MULTIMEDIA_TAGS = new Set(["video", "source", "audio"]);
74
+ const MULTIMEDIA_ALLOWED_ATTRS = new Set([
75
+ "src",
76
+ "type",
77
+ "controls",
78
+ "autoplay",
79
+ "loop",
80
+ "muted",
81
+ "playsinline",
82
+ "poster",
83
+ "width",
84
+ "height",
85
+ "preload",
86
+ ]);
87
+ const MULTIMEDIA_BOOLEAN_ATTRS = new Set(["controls", "autoplay", "loop", "muted", "playsinline"]);
88
+ const MULTIMEDIA_URI_ATTRS = new Set(["src", "poster"]);
89
+ const MULTIMEDIA_ALLOWED_URI_PATTERN = /^(?!javascript:|data:text\/html)/i;
90
+ const MULTIMEDIA_HTML_REGEX = /<\/?(video|source|audio)\b/i;
91
+
92
+ type HtmlNode = {
93
+ type: string;
94
+ name?: string;
95
+ attribs?: Record<string, string>;
96
+ children?: HtmlNode[];
97
+ data?: string;
98
+ };
99
+
100
  interface katexBlockToken extends Tokens.Generic {
101
  type: "katexBlock";
102
  raw: string;
 
266
  return hljs.highlightAuto(text).value;
267
  }
268
 
269
+ function sanitizeMediaUrl(value: string): string | undefined {
270
+ const trimmed = value.trim().replace(/>$/, "");
271
+ if (!MULTIMEDIA_ALLOWED_URI_PATTERN.test(trimmed)) return undefined;
272
+ return trimmed;
273
+ }
274
+
275
+ function serializeMediaAttributes(attribs?: Record<string, string>): string {
276
+ if (!attribs) return "";
277
+ const parts: string[] = [];
278
+ for (const [rawName, rawValue] of Object.entries(attribs)) {
279
+ const name = rawName.toLowerCase();
280
+ if (!MULTIMEDIA_ALLOWED_ATTRS.has(name)) continue;
281
+ if (MULTIMEDIA_BOOLEAN_ATTRS.has(name)) {
282
+ parts.push(name);
283
+ continue;
284
+ }
285
+ let value = rawValue ?? "";
286
+ if (MULTIMEDIA_URI_ATTRS.has(name)) {
287
+ const safeUrl = sanitizeMediaUrl(value);
288
+ if (!safeUrl) continue;
289
+ value = safeUrl;
290
+ }
291
+ parts.push(`${name}="${escapeHTML(value)}"`);
292
+ }
293
+ return parts.length ? ` ${parts.join(" ")}` : "";
294
+ }
295
+
296
+ function serializeMediaNode(node: HtmlNode, state: { hasDisallowedTag: boolean }): string {
297
+ if (node.type === "text") {
298
+ return escapeHTML(node.data ?? "");
299
+ }
300
+ if (node.type === "tag" || node.type === "script" || node.type === "style") {
301
+ const tagName = node.name?.toLowerCase() ?? "";
302
+ if (!MULTIMEDIA_TAGS.has(tagName)) {
303
+ state.hasDisallowedTag = true;
304
+ return "";
305
+ }
306
+ const attrs = serializeMediaAttributes(node.attribs);
307
+ if (tagName === "source") {
308
+ return `<source${attrs}>`;
309
+ }
310
+ const children = (node.children ?? [])
311
+ .map((child) => serializeMediaNode(child, state))
312
+ .join("");
313
+ return `<${tagName}${attrs}>${children}</${tagName}>`;
314
+ }
315
+ if (node.type === "comment") {
316
+ return "";
317
+ }
318
+ return "";
319
+ }
320
+
321
+ /**
322
+ * Sanitizes HTML to allow only video/audio/source tags with safe attributes.
323
+ * Uses htmlparser2 which works in Web Workers (no DOM needed).
324
+ * If any disallowed tags are found, escapes the entire input.
325
+ */
326
+ function sanitizeHtmlForMultimedia(html: string): string {
327
+ if (!MULTIMEDIA_HTML_REGEX.test(html)) {
328
+ return escapeHTML(html);
329
+ }
330
+ const document = parseDocument(html, {
331
+ lowerCaseAttributeNames: true,
332
+ lowerCaseTags: true,
333
+ recognizeSelfClosing: true,
334
+ }) as unknown as { children: HtmlNode[] };
335
+ const state = { hasDisallowedTag: false };
336
+ const sanitized = (document.children ?? [])
337
+ .map((child) => serializeMediaNode(child, state))
338
+ .join("");
339
+ if (state.hasDisallowedTag) {
340
+ return escapeHTML(html);
341
+ }
342
+ return sanitized;
343
+ }
344
+
345
  function createMarkedInstance(sources: SimpleSource[]): Marked {
346
  return new Marked({
347
  hooks: {
 
371
  }
372
  return `<img src="${safeSrc}" alt="${safeAlt}"${safeTitle} />`;
373
  },
374
+ html: (html) => sanitizeHtmlForMultimedia(html),
375
  },
376
  gfm: true,
377
  breaks: true,