@bquery/bquery 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +127 -27
  2. package/dist/batch-x7b2eZST.js +13 -0
  3. package/dist/batch-x7b2eZST.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +55 -3
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +31 -4
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +2 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +87 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-BhpuvPhy.js +170 -0
  41. package/dist/core-BhpuvPhy.js.map +1 -0
  42. package/dist/core.es.mjs +495 -489
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +2 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +87 -64
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +2 -2
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +2 -2
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.es.mjs +138 -68
  53. package/dist/index.es.mjs.map +1 -1
  54. package/dist/motion/animate.d.ts +25 -0
  55. package/dist/motion/animate.d.ts.map +1 -0
  56. package/dist/motion/easing.d.ts +30 -0
  57. package/dist/motion/easing.d.ts.map +1 -0
  58. package/dist/motion/flip.d.ts +55 -0
  59. package/dist/motion/flip.d.ts.map +1 -0
  60. package/dist/motion/index.d.ts +11 -138
  61. package/dist/motion/index.d.ts.map +1 -1
  62. package/dist/motion/keyframes.d.ts +21 -0
  63. package/dist/motion/keyframes.d.ts.map +1 -0
  64. package/dist/motion/reduced-motion.d.ts +12 -0
  65. package/dist/motion/reduced-motion.d.ts.map +1 -0
  66. package/dist/motion/scroll.d.ts +15 -0
  67. package/dist/motion/scroll.d.ts.map +1 -0
  68. package/dist/motion/spring.d.ts +42 -0
  69. package/dist/motion/spring.d.ts.map +1 -0
  70. package/dist/motion/stagger.d.ts +22 -0
  71. package/dist/motion/stagger.d.ts.map +1 -0
  72. package/dist/motion/timeline.d.ts +21 -0
  73. package/dist/motion/timeline.d.ts.map +1 -0
  74. package/dist/motion/transition.d.ts +22 -0
  75. package/dist/motion/transition.d.ts.map +1 -0
  76. package/dist/motion/types.d.ts +182 -0
  77. package/dist/motion/types.d.ts.map +1 -0
  78. package/dist/motion.es.mjs +320 -61
  79. package/dist/motion.es.mjs.map +1 -1
  80. package/dist/persisted-DHoi3uEs.js +278 -0
  81. package/dist/persisted-DHoi3uEs.js.map +1 -0
  82. package/dist/platform/storage.d.ts.map +1 -1
  83. package/dist/platform.es.mjs +12 -7
  84. package/dist/platform.es.mjs.map +1 -1
  85. package/dist/reactive/batch.d.ts +13 -0
  86. package/dist/reactive/batch.d.ts.map +1 -0
  87. package/dist/reactive/computed.d.ts +50 -0
  88. package/dist/reactive/computed.d.ts.map +1 -0
  89. package/dist/reactive/core.d.ts +72 -0
  90. package/dist/reactive/core.d.ts.map +1 -0
  91. package/dist/reactive/effect.d.ts +15 -0
  92. package/dist/reactive/effect.d.ts.map +1 -0
  93. package/dist/reactive/index.d.ts +2 -2
  94. package/dist/reactive/index.d.ts.map +1 -1
  95. package/dist/reactive/internals.d.ts +42 -0
  96. package/dist/reactive/internals.d.ts.map +1 -0
  97. package/dist/reactive/linked.d.ts +36 -0
  98. package/dist/reactive/linked.d.ts.map +1 -0
  99. package/dist/reactive/persisted.d.ts +14 -0
  100. package/dist/reactive/persisted.d.ts.map +1 -0
  101. package/dist/reactive/readonly.d.ts +26 -0
  102. package/dist/reactive/readonly.d.ts.map +1 -0
  103. package/dist/reactive/signal.d.ts +13 -312
  104. package/dist/reactive/signal.d.ts.map +1 -1
  105. package/dist/reactive/type-guards.d.ts +20 -0
  106. package/dist/reactive/type-guards.d.ts.map +1 -0
  107. package/dist/reactive/untrack.d.ts +29 -0
  108. package/dist/reactive/untrack.d.ts.map +1 -0
  109. package/dist/reactive/watch.d.ts +42 -0
  110. package/dist/reactive/watch.d.ts.map +1 -0
  111. package/dist/reactive.es.mjs +30 -163
  112. package/dist/reactive.es.mjs.map +1 -1
  113. package/dist/router/index.d.ts +6 -252
  114. package/dist/router/index.d.ts.map +1 -1
  115. package/dist/router/links.d.ts +44 -0
  116. package/dist/router/links.d.ts.map +1 -0
  117. package/dist/router/match.d.ts +20 -0
  118. package/dist/router/match.d.ts.map +1 -0
  119. package/dist/router/navigation.d.ts +45 -0
  120. package/dist/router/navigation.d.ts.map +1 -0
  121. package/dist/router/query.d.ts +16 -0
  122. package/dist/router/query.d.ts.map +1 -0
  123. package/dist/router/router.d.ts +34 -0
  124. package/dist/router/router.d.ts.map +1 -0
  125. package/dist/router/state.d.ts +27 -0
  126. package/dist/router/state.d.ts.map +1 -0
  127. package/dist/router/types.d.ts +88 -0
  128. package/dist/router/types.d.ts.map +1 -0
  129. package/dist/router/utils.d.ts +65 -0
  130. package/dist/router/utils.d.ts.map +1 -0
  131. package/dist/router.es.mjs +168 -132
  132. package/dist/router.es.mjs.map +1 -1
  133. package/dist/sanitize-Cxvxa-DX.js +283 -0
  134. package/dist/sanitize-Cxvxa-DX.js.map +1 -0
  135. package/dist/security/constants.d.ts +42 -0
  136. package/dist/security/constants.d.ts.map +1 -0
  137. package/dist/security/csp.d.ts +24 -0
  138. package/dist/security/csp.d.ts.map +1 -0
  139. package/dist/security/index.d.ts +4 -2
  140. package/dist/security/index.d.ts.map +1 -1
  141. package/dist/security/sanitize-core.d.ts +13 -0
  142. package/dist/security/sanitize-core.d.ts.map +1 -0
  143. package/dist/security/sanitize.d.ts +5 -57
  144. package/dist/security/sanitize.d.ts.map +1 -1
  145. package/dist/security/trusted-types.d.ts +25 -0
  146. package/dist/security/trusted-types.d.ts.map +1 -0
  147. package/dist/security/types.d.ts +36 -0
  148. package/dist/security/types.d.ts.map +1 -0
  149. package/dist/security.es.mjs +50 -277
  150. package/dist/security.es.mjs.map +1 -1
  151. package/dist/store/create-store.d.ts +15 -0
  152. package/dist/store/create-store.d.ts.map +1 -0
  153. package/dist/store/define-store.d.ts +28 -0
  154. package/dist/store/define-store.d.ts.map +1 -0
  155. package/dist/store/devtools.d.ts +22 -0
  156. package/dist/store/devtools.d.ts.map +1 -0
  157. package/dist/store/index.d.ts +10 -286
  158. package/dist/store/index.d.ts.map +1 -1
  159. package/dist/store/mapping.d.ts +28 -0
  160. package/dist/store/mapping.d.ts.map +1 -0
  161. package/dist/store/persisted.d.ts +13 -0
  162. package/dist/store/persisted.d.ts.map +1 -0
  163. package/dist/store/plugins.d.ts +13 -0
  164. package/dist/store/plugins.d.ts.map +1 -0
  165. package/dist/store/registry.d.ts +28 -0
  166. package/dist/store/registry.d.ts.map +1 -0
  167. package/dist/store/types.d.ts +71 -0
  168. package/dist/store/types.d.ts.map +1 -0
  169. package/dist/store/utils.d.ts +28 -0
  170. package/dist/store/utils.d.ts.map +1 -0
  171. package/dist/store/watch.d.ts +23 -0
  172. package/dist/store/watch.d.ts.map +1 -0
  173. package/dist/store.es.mjs +22 -224
  174. package/dist/store.es.mjs.map +1 -1
  175. package/dist/type-guards-BdKlYYlS.js +32 -0
  176. package/dist/type-guards-BdKlYYlS.js.map +1 -0
  177. package/dist/untrack-DNnnqdlR.js +6 -0
  178. package/dist/untrack-DNnnqdlR.js.map +1 -0
  179. package/dist/view/directives/bind.d.ts +7 -0
  180. package/dist/view/directives/bind.d.ts.map +1 -0
  181. package/dist/view/directives/class.d.ts +8 -0
  182. package/dist/view/directives/class.d.ts.map +1 -0
  183. package/dist/view/directives/for.d.ts +23 -0
  184. package/dist/view/directives/for.d.ts.map +1 -0
  185. package/dist/view/directives/html.d.ts +7 -0
  186. package/dist/view/directives/html.d.ts.map +1 -0
  187. package/dist/view/directives/if.d.ts +7 -0
  188. package/dist/view/directives/if.d.ts.map +1 -0
  189. package/dist/view/directives/index.d.ts +12 -0
  190. package/dist/view/directives/index.d.ts.map +1 -0
  191. package/dist/view/directives/model.d.ts +7 -0
  192. package/dist/view/directives/model.d.ts.map +1 -0
  193. package/dist/view/directives/on.d.ts +7 -0
  194. package/dist/view/directives/on.d.ts.map +1 -0
  195. package/dist/view/directives/ref.d.ts +7 -0
  196. package/dist/view/directives/ref.d.ts.map +1 -0
  197. package/dist/view/directives/show.d.ts +7 -0
  198. package/dist/view/directives/show.d.ts.map +1 -0
  199. package/dist/view/directives/style.d.ts +7 -0
  200. package/dist/view/directives/style.d.ts.map +1 -0
  201. package/dist/view/directives/text.d.ts +7 -0
  202. package/dist/view/directives/text.d.ts.map +1 -0
  203. package/dist/view/evaluate.d.ts +43 -0
  204. package/dist/view/evaluate.d.ts.map +1 -0
  205. package/dist/view/index.d.ts +3 -93
  206. package/dist/view/index.d.ts.map +1 -1
  207. package/dist/view/mount.d.ts +69 -0
  208. package/dist/view/mount.d.ts.map +1 -0
  209. package/dist/view/process.d.ts +26 -0
  210. package/dist/view/process.d.ts.map +1 -0
  211. package/dist/view/types.d.ts +36 -0
  212. package/dist/view/types.d.ts.map +1 -0
  213. package/dist/view.es.mjs +358 -251
  214. package/dist/view.es.mjs.map +1 -1
  215. package/dist/watch-DXXv3iAI.js +58 -0
  216. package/dist/watch-DXXv3iAI.js.map +1 -0
  217. package/package.json +14 -14
  218. package/src/component/component.ts +289 -0
  219. package/src/component/html.ts +53 -0
  220. package/src/component/index.ts +40 -414
  221. package/src/component/props.ts +116 -0
  222. package/src/component/types.ts +85 -0
  223. package/src/core/collection.ts +181 -7
  224. package/src/core/dom.ts +38 -0
  225. package/src/core/element.ts +59 -25
  226. package/src/core/index.ts +48 -4
  227. package/src/core/utils/array.ts +102 -0
  228. package/src/core/utils/function.ts +151 -0
  229. package/src/core/utils/index.ts +83 -0
  230. package/src/core/utils/misc.ts +82 -0
  231. package/src/core/utils/number.ts +78 -0
  232. package/src/core/utils/object.ts +206 -0
  233. package/src/core/utils/string.ts +112 -0
  234. package/src/core/utils/type-guards.ts +112 -0
  235. package/src/full.ts +187 -150
  236. package/src/index.ts +36 -36
  237. package/src/motion/animate.ts +113 -0
  238. package/src/motion/easing.ts +40 -0
  239. package/src/motion/flip.ts +176 -0
  240. package/src/motion/index.ts +41 -358
  241. package/src/motion/keyframes.ts +46 -0
  242. package/src/motion/reduced-motion.ts +17 -0
  243. package/src/motion/scroll.ts +57 -0
  244. package/src/motion/spring.ts +150 -0
  245. package/src/motion/stagger.ts +43 -0
  246. package/src/motion/timeline.ts +246 -0
  247. package/src/motion/transition.ts +51 -0
  248. package/src/motion/types.ts +198 -0
  249. package/src/platform/storage.ts +215 -208
  250. package/src/reactive/batch.ts +22 -0
  251. package/src/reactive/computed.ts +92 -0
  252. package/src/reactive/core.ts +114 -0
  253. package/src/reactive/effect.ts +54 -0
  254. package/src/reactive/index.ts +23 -22
  255. package/src/reactive/internals.ts +122 -0
  256. package/src/reactive/linked.ts +56 -0
  257. package/src/reactive/persisted.ts +74 -0
  258. package/src/reactive/readonly.ts +35 -0
  259. package/src/reactive/signal.ts +20 -520
  260. package/src/reactive/type-guards.ts +22 -0
  261. package/src/reactive/untrack.ts +31 -0
  262. package/src/reactive/watch.ts +73 -0
  263. package/src/router/index.ts +41 -718
  264. package/src/router/links.ts +130 -0
  265. package/src/router/match.ts +106 -0
  266. package/src/router/navigation.ts +71 -0
  267. package/src/router/query.ts +35 -0
  268. package/src/router/router.ts +211 -0
  269. package/src/router/state.ts +46 -0
  270. package/src/router/types.ts +93 -0
  271. package/src/router/utils.ts +116 -0
  272. package/src/security/constants.ts +209 -0
  273. package/src/security/csp.ts +77 -0
  274. package/src/security/index.ts +4 -12
  275. package/src/security/sanitize-core.ts +364 -0
  276. package/src/security/sanitize.ts +66 -625
  277. package/src/security/trusted-types.ts +69 -0
  278. package/src/security/types.ts +40 -0
  279. package/src/store/create-store.ts +329 -0
  280. package/src/store/define-store.ts +48 -0
  281. package/src/store/devtools.ts +45 -0
  282. package/src/store/index.ts +22 -848
  283. package/src/store/mapping.ts +73 -0
  284. package/src/store/persisted.ts +61 -0
  285. package/src/store/plugins.ts +32 -0
  286. package/src/store/registry.ts +51 -0
  287. package/src/store/types.ts +94 -0
  288. package/src/store/utils.ts +141 -0
  289. package/src/store/watch.ts +52 -0
  290. package/src/view/directives/bind.ts +23 -0
  291. package/src/view/directives/class.ts +70 -0
  292. package/src/view/directives/for.ts +275 -0
  293. package/src/view/directives/html.ts +19 -0
  294. package/src/view/directives/if.ts +30 -0
  295. package/src/view/directives/index.ts +11 -0
  296. package/src/view/directives/model.ts +56 -0
  297. package/src/view/directives/on.ts +41 -0
  298. package/src/view/directives/ref.ts +41 -0
  299. package/src/view/directives/show.ts +26 -0
  300. package/src/view/directives/style.ts +47 -0
  301. package/src/view/directives/text.ts +15 -0
  302. package/src/view/evaluate.ts +290 -0
  303. package/src/view/index.ts +112 -1041
  304. package/src/view/mount.ts +200 -0
  305. package/src/view/process.ts +92 -0
  306. package/src/view/types.ts +44 -0
  307. package/dist/core/utils.d.ts +0 -313
  308. package/dist/core/utils.d.ts.map +0 -1
  309. package/src/core/utils.ts +0 -444
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Core HTML sanitization logic.
3
+ *
4
+ * @module bquery/security
5
+ * @internal
6
+ */
7
+
8
+ import {
9
+ DANGEROUS_ATTR_PREFIXES,
10
+ DANGEROUS_PROTOCOLS,
11
+ DANGEROUS_TAGS,
12
+ DEFAULT_ALLOWED_ATTRIBUTES,
13
+ DEFAULT_ALLOWED_TAGS,
14
+ RESERVED_IDS,
15
+ } from './constants';
16
+ import type { SanitizeOptions } from './types';
17
+
18
+ /**
19
+ * Check if an attribute name is allowed.
20
+ * @internal
21
+ */
22
+ const isAllowedAttribute = (
23
+ name: string,
24
+ allowedSet: Set<string>,
25
+ allowDataAttrs: boolean
26
+ ): boolean => {
27
+ const lowerName = name.toLowerCase();
28
+
29
+ // Check dangerous prefixes
30
+ for (const prefix of DANGEROUS_ATTR_PREFIXES) {
31
+ if (lowerName.startsWith(prefix)) return false;
32
+ }
33
+
34
+ // Check data attributes
35
+ if (allowDataAttrs && lowerName.startsWith('data-')) return true;
36
+
37
+ // Check aria attributes (allowed by default)
38
+ if (lowerName.startsWith('aria-')) return true;
39
+
40
+ // Check explicit allow list
41
+ return allowedSet.has(lowerName);
42
+ };
43
+
44
+ /**
45
+ * Check if an ID/name value could cause DOM clobbering.
46
+ * @internal
47
+ */
48
+ const isSafeIdOrName = (value: string): boolean => {
49
+ const lowerValue = value.toLowerCase().trim();
50
+ return !RESERVED_IDS.has(lowerValue);
51
+ };
52
+
53
+ /**
54
+ * Normalize URL by removing control characters, whitespace, and Unicode tricks.
55
+ * Enhanced to prevent various bypass techniques.
56
+ * @internal
57
+ */
58
+ const normalizeUrl = (value: string): string =>
59
+ value
60
+ // Remove null bytes and control characters
61
+ .replace(/[\u0000-\u001F\u007F]+/g, '')
62
+ // Remove zero-width characters that could hide malicious content
63
+ .replace(/[\u200B-\u200D\uFEFF\u2028\u2029]+/g, '')
64
+ // Remove escaped Unicode sequences
65
+ .replace(/\\u[\da-fA-F]{4}/g, '')
66
+ // Remove whitespace
67
+ .replace(/\s+/g, '')
68
+ // Normalize case
69
+ .toLowerCase();
70
+
71
+ /**
72
+ * Check if a URL value is safe.
73
+ * @internal
74
+ */
75
+ const isSafeUrl = (value: string): boolean => {
76
+ const normalized = normalizeUrl(value);
77
+ for (const protocol of DANGEROUS_PROTOCOLS) {
78
+ if (normalized.startsWith(protocol)) return false;
79
+ }
80
+ return true;
81
+ };
82
+
83
+ /**
84
+ * Check if a srcset attribute value is safe.
85
+ * srcset contains comma-separated entries of "url [descriptor]".
86
+ * Each individual URL must be validated.
87
+ * @internal
88
+ */
89
+ const isSafeSrcset = (value: string): boolean => {
90
+ const entries = value.split(',');
91
+ for (const entry of entries) {
92
+ const url = entry.trim().split(/\s+/)[0];
93
+ if (url && !isSafeUrl(url)) return false;
94
+ }
95
+ return true;
96
+ };
97
+
98
+ /**
99
+ * Check if a URL is external (different origin).
100
+ * @internal
101
+ */
102
+ const isExternalUrl = (url: string): boolean => {
103
+ try {
104
+ // Normalize URL by trimming whitespace
105
+ const trimmedUrl = url.trim();
106
+
107
+ // Protocol-relative URLs (//example.com) are always external.
108
+ // CRITICAL: This check must run before the relative-URL check below;
109
+ // otherwise, a protocol-relative URL like "//evil.com" would be treated
110
+ // as a non-http(s) relative URL and incorrectly classified as same-origin.
111
+ // Handling them up front guarantees correct security classification.
112
+ if (trimmedUrl.startsWith('//')) {
113
+ return true;
114
+ }
115
+
116
+ // Normalize URL for case-insensitive protocol checks
117
+ const lowerUrl = trimmedUrl.toLowerCase();
118
+
119
+ // Check for non-http(s) protocols which are considered external/special
120
+ // (mailto:, tel:, ftp:, etc.)
121
+ const hasProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmedUrl);
122
+ if (hasProtocol && !lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
123
+ // These are special protocols, not traditional "external" links
124
+ // but we treat them as external for security consistency
125
+ return true;
126
+ }
127
+
128
+ // Relative URLs are not external
129
+ if (!lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
130
+ return false;
131
+ }
132
+
133
+ // In non-browser environments (e.g., Node.js), treat all absolute URLs as external
134
+ if (typeof window === 'undefined' || !window.location) {
135
+ return true;
136
+ }
137
+
138
+ const urlObj = new URL(trimmedUrl, window.location.href);
139
+ return urlObj.origin !== window.location.origin;
140
+ } catch {
141
+ // If URL parsing fails, treat as potentially external for safety
142
+ return true;
143
+ }
144
+ };
145
+
146
+ /**
147
+ * Parse an HTML string into a Document using DOMParser.
148
+ * This helper is intentionally separated to make the control-flow around HTML parsing
149
+ * explicit for static analysis tools. It should ONLY be called when the input is
150
+ * known to contain HTML syntax (angle brackets).
151
+ *
152
+ * DOMParser creates an inert document where scripts don't execute, making it safe
153
+ * for parsing untrusted HTML that will subsequently be sanitized.
154
+ *
155
+ * @param htmlContent - A string that is known to contain HTML markup (has < or >)
156
+ * @returns The parsed Document
157
+ * @internal
158
+ */
159
+ const parseHtmlDocument = (htmlContent: string): Document => {
160
+ const parser = new DOMParser();
161
+ // Parse as a full HTML document in an inert context; scripts won't execute
162
+ return parser.parseFromString(htmlContent, 'text/html');
163
+ };
164
+
165
+ /**
166
+ * Safely parse HTML string into a DocumentFragment using DOMParser.
167
+ * DOMParser is preferred over innerHTML for security as it creates an inert document
168
+ * where scripts don't execute and provides better static analysis recognition.
169
+ *
170
+ * This function includes input normalization to satisfy static analysis tools:
171
+ * - Coerces input to string and trims whitespace
172
+ * - For plain text (no HTML tags), creates a Text node directly without parsing
173
+ * - Only invokes DOMParser for actual HTML-like content via parseHtmlDocument
174
+ *
175
+ * The separation between plain text handling and HTML parsing is intentional:
176
+ * DOM text that contains no HTML syntax is never fed into an HTML parser,
177
+ * preventing "DOM text reinterpreted as HTML" issues.
178
+ *
179
+ * @internal
180
+ */
181
+ const parseHtmlSafely = (html: string): DocumentFragment => {
182
+ // Step 1: Normalize input - coerce to string and trim
183
+ // This defensive check handles edge cases even though TypeScript says it's a string
184
+ const normalizedHtml = (typeof html === 'string' ? html : String(html ?? '')).trim();
185
+
186
+ // Step 2: Create the fragment that will hold our result
187
+ const fragment = document.createDocumentFragment();
188
+
189
+ // Step 3: Early return for empty input
190
+ if (normalizedHtml.length === 0) {
191
+ return fragment;
192
+ }
193
+
194
+ // Step 4: If input contains no angle brackets, it's plain text - no HTML parsing needed.
195
+ // Plain text is handled as a Text node, never passed to an HTML parser.
196
+ // This explicitly prevents "DOM text reinterpreted as HTML" for purely textual inputs.
197
+ const containsHtmlSyntax = normalizedHtml.includes('<') || normalizedHtml.includes('>');
198
+ if (!containsHtmlSyntax) {
199
+ fragment.appendChild(document.createTextNode(normalizedHtml));
200
+ return fragment;
201
+ }
202
+
203
+ // Step 5: Input contains HTML syntax - parse it via the dedicated HTML parsing helper.
204
+ // This separation makes the data-flow explicit: only strings with HTML syntax
205
+ // are passed to DOMParser, satisfying static analysis requirements.
206
+ const doc = parseHtmlDocument(normalizedHtml);
207
+
208
+ // Move all children from the document body into the fragment.
209
+ // This avoids interpolating untrusted HTML into an outer wrapper string.
210
+ const body = doc.body;
211
+
212
+ if (!body) {
213
+ return fragment;
214
+ }
215
+
216
+ while (body.firstChild) {
217
+ fragment.appendChild(body.firstChild);
218
+ }
219
+
220
+ return fragment;
221
+ };
222
+
223
+ /**
224
+ * Core sanitization logic (without Trusted Types wrapper).
225
+ * @internal
226
+ */
227
+ export const sanitizeHtmlCore = (html: string, options: SanitizeOptions = {}): string => {
228
+ const {
229
+ allowTags = [],
230
+ allowAttributes = [],
231
+ allowDataAttributes = true,
232
+ stripAllTags = false,
233
+ } = options;
234
+
235
+ // Build combined allow sets (excluding dangerous tags even if specified)
236
+ const allowedTags = new Set(
237
+ [...DEFAULT_ALLOWED_TAGS, ...allowTags.map((t) => t.toLowerCase())].filter(
238
+ (tag) => !DANGEROUS_TAGS.has(tag)
239
+ )
240
+ );
241
+ const allowedAttrs = new Set([
242
+ ...DEFAULT_ALLOWED_ATTRIBUTES,
243
+ ...allowAttributes.map((a) => a.toLowerCase()),
244
+ ]);
245
+
246
+ // Use DOMParser for safe HTML parsing (inert context, no script execution)
247
+ const fragment = parseHtmlSafely(html);
248
+
249
+ if (stripAllTags) {
250
+ return fragment.textContent ?? '';
251
+ }
252
+
253
+ // Walk the DOM tree
254
+ const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT);
255
+
256
+ const toRemove: Element[] = [];
257
+
258
+ while (walker.nextNode()) {
259
+ const el = walker.currentNode as Element;
260
+ const tagName = el.tagName.toLowerCase();
261
+
262
+ // Remove explicitly dangerous tags even if in allow list
263
+ if (DANGEROUS_TAGS.has(tagName)) {
264
+ toRemove.push(el);
265
+ continue;
266
+ }
267
+
268
+ // Remove disallowed tags entirely
269
+ if (!allowedTags.has(tagName)) {
270
+ toRemove.push(el);
271
+ continue;
272
+ }
273
+
274
+ // Process attributes
275
+ const attrsToRemove: string[] = [];
276
+ for (const attr of Array.from(el.attributes)) {
277
+ const attrName = attr.name.toLowerCase();
278
+
279
+ // Check if attribute is allowed
280
+ if (!isAllowedAttribute(attrName, allowedAttrs, allowDataAttributes)) {
281
+ attrsToRemove.push(attr.name);
282
+ continue;
283
+ }
284
+
285
+ // Check for DOM clobbering on id and name attributes
286
+ if ((attrName === 'id' || attrName === 'name') && !isSafeIdOrName(attr.value)) {
287
+ attrsToRemove.push(attr.name);
288
+ continue;
289
+ }
290
+
291
+ // Validate URL attributes
292
+ if (
293
+ (attrName === 'href' || attrName === 'src' || attrName === 'action') &&
294
+ !isSafeUrl(attr.value)
295
+ ) {
296
+ attrsToRemove.push(attr.name);
297
+ continue;
298
+ }
299
+
300
+ // Validate srcset URLs individually
301
+ if (attrName === 'srcset' && !isSafeSrcset(attr.value)) {
302
+ attrsToRemove.push(attr.name);
303
+ }
304
+ }
305
+
306
+ // Remove disallowed attributes
307
+ for (const attrName of attrsToRemove) {
308
+ el.removeAttribute(attrName);
309
+ }
310
+
311
+ // Add rel="noopener noreferrer" to external links for security
312
+ if (tagName === 'a') {
313
+ const href = el.getAttribute('href');
314
+ const target = el.getAttribute('target');
315
+ const hasTargetBlank = target?.toLowerCase() === '_blank';
316
+ const isExternal = href && isExternalUrl(href);
317
+
318
+ // Add security attributes to links opening in new window or external links
319
+ if (hasTargetBlank || isExternal) {
320
+ const existingRel = el.getAttribute('rel');
321
+ const relValues = new Set(existingRel ? existingRel.split(/\s+/).filter(Boolean) : []);
322
+
323
+ // Add noopener and noreferrer
324
+ relValues.add('noopener');
325
+ relValues.add('noreferrer');
326
+
327
+ el.setAttribute('rel', Array.from(relValues).join(' '));
328
+ }
329
+ }
330
+ }
331
+
332
+ // Remove disallowed elements
333
+ for (const el of toRemove) {
334
+ el.remove();
335
+ }
336
+
337
+ // Serialize the sanitized fragment to HTML string.
338
+ // We use a temporary container to get the innerHTML of the fragment.
339
+ const serializeFragment = (frag: DocumentFragment): string => {
340
+ const container = document.createElement('div');
341
+ container.appendChild(frag.cloneNode(true));
342
+ return container.innerHTML;
343
+ };
344
+
345
+ // Double-parse to prevent mutation XSS (mXSS).
346
+ // Browsers may normalize HTML during serialization in ways that could create
347
+ // new dangerous content when re-parsed. By re-parsing the sanitized output
348
+ // and verifying stability, we ensure the final HTML is safe.
349
+ const firstPass = serializeFragment(fragment);
350
+
351
+ // Re-parse through DOMParser for mXSS detection.
352
+ // Using DOMParser instead of innerHTML for security.
353
+ const verifyFragment = parseHtmlSafely(firstPass);
354
+ const secondPass = serializeFragment(verifyFragment);
355
+
356
+ // Verify stability: if content mutates between parses, it indicates mXSS attempt
357
+ if (firstPass !== secondPass) {
358
+ // Content mutated during re-parse - potential mXSS detected.
359
+ // Return safely escaped text content as fallback.
360
+ return fragment.textContent ?? '';
361
+ }
362
+
363
+ return secondPass;
364
+ };