acrylblends 0.6.2 → 0.6.3

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 (181) hide show
  1. package/README.md +4 -1
  2. package/docs/framework/README.md +3 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/cordis/acryl-in-a-cordis-app.md +6 -0
  5. package/docs/framework/getting-started/basic.md +1 -0
  6. package/docs/framework/getting-started/desktop.md +41 -0
  7. package/docs/framework/guides/jsx-dom.md +0 -3
  8. package/docs/framework/guides/jsx.md +0 -3
  9. package/docs/framework/helpers/css.md +0 -3
  10. package/docs/framework/upstream.json +3 -3
  11. package/lib/accepts.d.ts +1 -1
  12. package/lib/acryl-base.d.ts +1 -1
  13. package/lib/adapter.d.ts +1 -1
  14. package/lib/{app-BdrVHBlJ.d.ts → app-wFwpg-bt.d.ts} +1 -1
  15. package/lib/base-BpQy2Rk7.d.ts +783 -0
  16. package/lib/base-ddiVmiE7.js +721 -0
  17. package/lib/basic-auth.d.ts +1 -1
  18. package/lib/bearer-auth.d.ts +1 -1
  19. package/lib/body-limit.d.ts +1 -1
  20. package/lib/cache.d.ts +1 -1
  21. package/lib/client.d.ts +2 -2
  22. package/lib/combine.d.ts +1 -1
  23. package/lib/common-D8Z4JRCw.d.ts +46 -0
  24. package/lib/components-Ce5FRyHU.js +268 -0
  25. package/lib/components-DYZdxJIJ.js +24 -0
  26. package/lib/compress.d.ts +1 -1
  27. package/lib/conninfo.d.ts +2 -2
  28. package/lib/constants-BR9jM9BC.js +9 -0
  29. package/lib/{context-B5Y1lLha.d.ts → context-DVQBjuns.d.ts} +1 -1
  30. package/lib/context-DxFdY_un.d.ts +250 -0
  31. package/lib/context-storage.d.ts +1 -1
  32. package/lib/{cookie-D9_nQbsM.d.ts → cookie-CCelEqky.d.ts} +1 -1
  33. package/lib/cookie.d.ts +3 -3
  34. package/lib/cors.d.ts +1 -1
  35. package/lib/csrf.d.ts +1 -1
  36. package/lib/css-D2jb7X8b.js +320 -0
  37. package/lib/css.d.ts +82 -0
  38. package/lib/css.js +134 -0
  39. package/lib/dev.d.ts +2 -2
  40. package/lib/etag.d.ts +1 -1
  41. package/lib/factory.d.ts +2 -2
  42. package/lib/hooks-Bq5q9IMl.js +762 -0
  43. package/lib/html-SllAe0YT.d.ts +46 -0
  44. package/lib/html.d.ts +1 -1
  45. package/lib/index-C850MeBA.d.ts +94 -0
  46. package/lib/{index-m12zcmph.d.ts → index-CUTfU69R.d.ts} +3 -3
  47. package/lib/{index-DtEoak1i.d.ts → index-Cqcdluyn.d.ts} +2 -2
  48. package/lib/{index-Kqou8Fjh.d.ts → index-CsvB-F9j.d.ts} +2 -2
  49. package/lib/index.d.ts +4 -4
  50. package/lib/ip-restriction.d.ts +2 -2
  51. package/lib/jsx/dom/client.d.ts +30 -0
  52. package/lib/jsx/dom/client.js +49 -0
  53. package/lib/jsx/dom/css.d.ts +84 -0
  54. package/lib/jsx/dom/css.js +2 -0
  55. package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
  56. package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
  57. package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
  58. package/lib/jsx/dom/jsx-runtime.js +2 -0
  59. package/lib/jsx/dom/server.d.ts +105 -0
  60. package/lib/jsx/dom/server.js +37 -0
  61. package/lib/jsx/dom.d.ts +5 -0
  62. package/lib/jsx/dom.js +74 -0
  63. package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
  64. package/lib/jsx/jsx-dev-runtime.js +14 -0
  65. package/lib/jsx/jsx-runtime.d.ts +10 -0
  66. package/lib/jsx/jsx-runtime.js +30 -0
  67. package/lib/jsx/streaming.d.ts +2 -0
  68. package/lib/jsx/streaming.js +2 -0
  69. package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
  70. package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
  71. package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
  72. package/lib/jsx.d.ts +3 -0
  73. package/lib/jsx.js +47 -0
  74. package/lib/jwk.d.ts +4 -4
  75. package/lib/{jwt-BA65QhnJ.d.ts → jwt-BARhotDz.d.ts} +1 -1
  76. package/lib/jwt.d.ts +4 -4
  77. package/lib/language.d.ts +1 -1
  78. package/lib/logger.d.ts +1 -1
  79. package/lib/loopback-only.d.ts +1 -1
  80. package/lib/method-not-allowed.d.ts +2 -2
  81. package/lib/method-override.d.ts +2 -2
  82. package/lib/mount.d.ts +1 -1
  83. package/lib/node-server/conninfo.d.ts +1 -1
  84. package/lib/node-server/serve-static.d.ts +1 -1
  85. package/lib/node-server/serve-static.js +1 -1
  86. package/lib/node-server.d.ts +1 -1
  87. package/lib/node-server.js +1 -1
  88. package/lib/powered-by.d.ts +1 -1
  89. package/lib/pretty-json.d.ts +1 -1
  90. package/lib/proxy.d.ts +1 -1
  91. package/lib/quick.d.ts +1 -1
  92. package/lib/request-id.d.ts +1 -1
  93. package/lib/request.d.ts +1 -1
  94. package/lib/route.d.ts +1 -1
  95. package/lib/schema-validator.d.ts +1 -1
  96. package/lib/secure-headers.d.ts +1 -1
  97. package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
  98. package/lib/serve-static.d.ts +1 -1
  99. package/lib/streaming.d.ts +1 -1
  100. package/lib/testing.d.ts +4 -4
  101. package/lib/timeout.d.ts +1 -1
  102. package/lib/timing.d.ts +1 -1
  103. package/lib/tiny.d.ts +1 -1
  104. package/lib/trailing-slash.d.ts +1 -1
  105. package/lib/{types-Bd29gh2o.d.ts → types-CQiGmsWW.d.ts} +3 -3
  106. package/lib/{types-D9DEXMjE.d.ts → types-Lqx5BxQF.d.ts} +8 -1
  107. package/lib/{types-ZM9gIiKh.d.ts → types-nEL8VPDC.d.ts} +1 -1
  108. package/lib/types.d.ts +1 -1
  109. package/lib/utils/body.d.ts +1 -1
  110. package/lib/utils/cookie.d.ts +1 -1
  111. package/lib/utils/crypto.d.ts +1 -1
  112. package/lib/utils/html.d.ts +1 -45
  113. package/lib/utils/ipaddr.d.ts +1 -1
  114. package/lib/utils/jwt.d.ts +1 -1
  115. package/lib/utils/types.d.ts +2 -2
  116. package/lib/validator.d.ts +2 -2
  117. package/lib/ws.d.ts +1 -1
  118. package/package.json +45 -1
  119. package/plugins/acryl-inspect/upstream.json +1 -1
  120. package/plugins/acryl-node-server/upstream.json +1 -1
  121. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
  122. package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
  123. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
  124. package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
  125. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
  126. package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
  127. package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
  128. package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
  129. package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
  130. package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
  131. package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
  132. package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
  133. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
  134. package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
  135. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
  136. package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
  137. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
  138. package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
  139. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
  140. package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
  141. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
  142. package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
  143. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
  144. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
  145. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
  146. package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
  147. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
  148. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
  149. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
  150. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
  151. package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
  152. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
  153. package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
  154. package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
  155. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
  156. package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
  157. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
  158. package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
  159. package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
  160. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
  161. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
  162. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
  163. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
  164. package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
  165. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
  166. package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
  167. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
  168. package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
  169. package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
  170. package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
  171. package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
  172. package/plugins/acryl-router/src/core/utils/types.ts +6 -0
  173. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  174. package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
  175. package/plugins/acryl-router/upstream.json +3 -3
  176. package/plugins/acryl-router-http/upstream.json +1 -1
  177. package/plugins/acryl-state/upstream.json +1 -1
  178. package/plugins/desktop/upstream.json +1 -1
  179. package/plugins/desktop-web/upstream.json +1 -1
  180. package/plugins/save/plugin.json +1 -1
  181. package/templates/todo/upstream.json +1 -1
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @module
3
+ * This module enables JSX to supports streaming Response.
4
+ */
5
+
6
+ import { raw } from '../helper/html/index.ts'
7
+ import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'
8
+ import type { HtmlEscapedString } from '../utils/html.ts'
9
+ import type { Child, FC, PropsWithChildren, Context as JSXContext } from './index.ts'
10
+ import { isUntrustedObject, JSXNode, renderChildren, renderUntrustedObject } from './base.ts'
11
+ import { childrenToString } from './components.ts'
12
+ import { DOM_RENDERER, DOM_STASH } from './constants.ts'
13
+ import { captureRenderContext, createContext, useContext } from './context.ts'
14
+ import { Suspense as SuspenseDomRenderer } from './dom/components.ts'
15
+ import { buildDataStack } from './dom/render.ts'
16
+ import type { HasRenderToDom, NodeObject } from './dom/render.ts'
17
+
18
+ /**
19
+ * Used to specify nonce for scripts generated by `Suspense` and `ErrorBoundary`.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <StreamingContext.Provider value={{ scriptNonce: 'test-nonce' }}>
24
+ * <Suspense fallback={<p>Loading...</p>}>
25
+ * <Content />
26
+ * </Suspense>
27
+ * </StreamingContext.Provider>
28
+ * ```
29
+ */
30
+ export const StreamingContext: JSXContext<{ scriptNonce: string } | null> = createContext<{
31
+ scriptNonce: string
32
+ } | null>(null)
33
+
34
+ let suspenseCounter = 0
35
+
36
+ const serializeBoundaryChild = (child: Child): string | Promise<string> =>
37
+ typeof child === 'string' || Array.isArray(child)
38
+ ? renderChildren([child])
39
+ : child == null || typeof child === 'boolean'
40
+ ? ''
41
+ : isUntrustedObject(child)
42
+ ? renderUntrustedObject(child)
43
+ : child.toString()
44
+
45
+ /**
46
+ * @experimental
47
+ * `Suspense` is an experimental feature.
48
+ * The API might be changed.
49
+ */
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = async ({
52
+ children,
53
+ fallback,
54
+ }) => {
55
+ if (!Array.isArray(children)) {
56
+ children = [children]
57
+ }
58
+
59
+ const nonce = useContext(StreamingContext)?.scriptNonce
60
+
61
+ let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
62
+
63
+ // for use() hook
64
+ const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject
65
+ const popNodeStack = () => {
66
+ buildDataStack.pop()
67
+ }
68
+
69
+ try {
70
+ stackNode[DOM_STASH][0] = 0
71
+ buildDataStack.push([[], stackNode])
72
+ resArray = children.map(serializeBoundaryChild) as HtmlEscapedString[]
73
+ } catch (e) {
74
+ if (e instanceof Promise) {
75
+ // Capture before deferring: on the fallback path the render context is
76
+ // observable here but not in the `.then()` below.
77
+ const resume = captureRenderContext()
78
+ resArray = [
79
+ e.then(() =>
80
+ resume(() => {
81
+ stackNode[DOM_STASH][0] = 0
82
+ buildDataStack.push([[], stackNode])
83
+ return childrenToString(children as Child[]).finally(popNodeStack)
84
+ })
85
+ ),
86
+ ] as Promise<HtmlEscapedString[]>[]
87
+ } else {
88
+ throw e
89
+ }
90
+ } finally {
91
+ popNodeStack()
92
+ }
93
+
94
+ if (resArray.some((res) => (res as {}) instanceof Promise)) {
95
+ const index = suspenseCounter++
96
+ const fallbackStr = await (typeof fallback === 'string' || Array.isArray(fallback)
97
+ ? renderChildren([fallback])
98
+ : isUntrustedObject(fallback)
99
+ ? renderUntrustedObject(fallback)
100
+ : fallback.toString())
101
+ return raw(`<template id="H:${index}"></template>${fallbackStr}<!--/$-->`, [
102
+ ...(fallbackStr.callbacks || []),
103
+ ({ phase, buffer, context }) => {
104
+ if (phase === HtmlEscapedCallbackPhase.BeforeStream) {
105
+ return
106
+ }
107
+ return Promise.all(resArray).then(async (htmlArray) => {
108
+ htmlArray = htmlArray.flat()
109
+ const content = htmlArray.join('')
110
+ if (buffer) {
111
+ buffer[0] = buffer[0].replace(
112
+ new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`, 's'),
113
+ () => content
114
+ )
115
+ }
116
+ let html = buffer
117
+ ? ''
118
+ : `<template data-acryl-target="H:${index}">${content}</template><script${
119
+ nonce ? ` nonce="${nonce}"` : ''
120
+ }>
121
+ ((d,c,n) => {
122
+ c=d.currentScript.previousSibling
123
+ d=d.getElementById('H:${index}')
124
+ if(!d)return
125
+ do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
126
+ d.replaceWith(c.content)
127
+ })(document)
128
+ </script>`
129
+
130
+ const callbacks = htmlArray
131
+ .map((html) => (html as HtmlEscapedString).callbacks || [])
132
+ .flat()
133
+ if (!callbacks.length) {
134
+ return html
135
+ }
136
+
137
+ if (phase === HtmlEscapedCallbackPhase.Stream) {
138
+ html = await resolveCallback(html, HtmlEscapedCallbackPhase.BeforeStream, true, context)
139
+ }
140
+
141
+ return raw(html, callbacks)
142
+ })
143
+ },
144
+ ])
145
+ } else {
146
+ return raw(resArray.join(''))
147
+ }
148
+ }
149
+ ;(Suspense as HasRenderToDom)[DOM_RENDERER] = SuspenseDomRenderer
150
+
151
+ const textEncoder = new TextEncoder()
152
+ /**
153
+ * @experimental
154
+ * `renderToReadableStream()` is an experimental feature.
155
+ * The API might be changed.
156
+ */
157
+ export const renderToReadableStream = (
158
+ content: HtmlEscapedString | JSXNode | Promise<HtmlEscapedString>,
159
+ onError: (e: unknown) => string | void = console.trace
160
+ ): ReadableStream<Uint8Array> => {
161
+ let cancelled = false
162
+ const reader = new ReadableStream<Uint8Array>({
163
+ async start(controller) {
164
+ try {
165
+ if (content instanceof JSXNode) {
166
+ // aJSXNode.toString() returns a string or Promise<string> and string is already escaped
167
+ content = content.toString() as HtmlEscapedString | Promise<HtmlEscapedString>
168
+ }
169
+ const context = typeof content === 'object' ? content : {}
170
+ const resolved = await resolveCallback(
171
+ content,
172
+ HtmlEscapedCallbackPhase.BeforeStream,
173
+ true,
174
+ context
175
+ )
176
+ if (!cancelled) {
177
+ controller.enqueue(textEncoder.encode(resolved))
178
+ }
179
+
180
+ let resolvedCount = 0
181
+ const callbacks: Promise<void>[] = []
182
+ const then = (promise: Promise<string>) => {
183
+ callbacks.push(
184
+ promise
185
+ .catch((err) => {
186
+ console.log(err)
187
+ onError(err)
188
+ return ''
189
+ })
190
+ .then(async (res) => {
191
+ res = await resolveCallback(
192
+ res,
193
+ HtmlEscapedCallbackPhase.BeforeStream,
194
+ true,
195
+ context
196
+ )
197
+ ;(res as HtmlEscapedString).callbacks
198
+ ?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context }))
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
+ .filter<Promise<string>>(Boolean as any)
201
+ .forEach(then)
202
+ resolvedCount++
203
+ if (!cancelled) {
204
+ controller.enqueue(textEncoder.encode(res))
205
+ }
206
+ })
207
+ )
208
+ }
209
+ ;(resolved as HtmlEscapedString).callbacks
210
+ ?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context }))
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ .filter<Promise<string>>(Boolean as any)
213
+ .forEach(then)
214
+ while (resolvedCount !== callbacks.length) {
215
+ await Promise.all(callbacks)
216
+ }
217
+ } catch (e) {
218
+ // maybe the connection was closed
219
+ onError(e)
220
+ }
221
+
222
+ if (!cancelled) {
223
+ controller.close()
224
+ }
225
+ },
226
+ cancel() {
227
+ cancelled = true
228
+ },
229
+ })
230
+ return reader
231
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * All types exported from "acryl-router/jsx" are in this file.
3
+ */
4
+ import type { Child, JSXNode } from './base.ts'
5
+ import type { JSX } from './intrinsic-elements.ts'
6
+ import type { DomEvent } from '../utils/types.ts'
7
+
8
+ export type { Child, JSXNode, FC } from './base.ts'
9
+ export type { RefObject } from './hooks/index.ts'
10
+ export type { Context } from './context.ts'
11
+
12
+ export type PropsWithChildren<P = unknown> = P & { children?: Child | undefined }
13
+ export type CSSProperties = JSX.CSSProperties
14
+
15
+ /**
16
+ * React types
17
+ */
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ type ReactElement<P = any, T = string | Function> = JSXNode & {
21
+ type: T
22
+ props: P
23
+ key: string | null
24
+ }
25
+ type ReactNode = ReactElement | string | number | boolean | null | undefined
26
+ type ComponentClass<_P = {}, _S = {}> = unknown
27
+
28
+ export type { ReactElement, ReactNode, ComponentClass }
29
+
30
+ export type Event = globalThis.Event
31
+ export type MouseEvent = DomEvent<'MouseEvent'>
32
+ export type KeyboardEvent = DomEvent<'KeyboardEvent'>
33
+ export type FocusEvent = DomEvent<'FocusEvent'>
34
+ export type ClipboardEvent = DomEvent<'ClipboardEvent'>
35
+ export type InputEvent = DomEvent<'InputEvent'>
36
+ export type PointerEvent = DomEvent<'PointerEvent'>
37
+ export type TouchEvent = DomEvent<'TouchEvent'>
38
+ export type WheelEvent = DomEvent<'WheelEvent'>
39
+ export type AnimationEvent = DomEvent<'AnimationEvent'>
40
+ export type TransitionEvent = DomEvent<'TransitionEvent'>
41
+ export type DragEvent = DomEvent<'DragEvent'>
@@ -0,0 +1,366 @@
1
+ import {
2
+ isValidAttributeName,
3
+ isValidTagName,
4
+ normalizeIntrinsicElementKey,
5
+ styleObjectForEach,
6
+ } from './utils.ts'
7
+
8
+ describe('normalizeIntrinsicElementKey', () => {
9
+ test.each`
10
+ key | expected
11
+ ${'className'} | ${'class'}
12
+ ${'htmlFor'} | ${'for'}
13
+ ${'crossOrigin'} | ${'crossorigin'}
14
+ ${'httpEquiv'} | ${'http-equiv'}
15
+ ${'itemProp'} | ${'itemprop'}
16
+ ${'fetchPriority'} | ${'fetchpriority'}
17
+ ${'noModule'} | ${'nomodule'}
18
+ ${'formAction'} | ${'formaction'}
19
+ ${'href'} | ${'href'}
20
+ `('should convert $key to $expected', ({ key, expected }) => {
21
+ expect(normalizeIntrinsicElementKey(key)).toBe(expected)
22
+ })
23
+ })
24
+
25
+ describe('isValidAttributeName', () => {
26
+ test.each`
27
+ name
28
+ ${'class'}
29
+ ${'id'}
30
+ ${'href'}
31
+ ${'data-foo'}
32
+ ${'aria-label'}
33
+ ${'onclick'}
34
+ ${'viewBox'}
35
+ ${'xml:lang'}
36
+ `('should return true for valid name "$name"', ({ name }) => {
37
+ expect(isValidAttributeName(name)).toBe(true)
38
+ })
39
+
40
+ test.each`
41
+ name | description
42
+ ${''} | ${'empty string'}
43
+ ${'" onfocus="alert(1)'} | ${'double quote'}
44
+ ${"' onfocus='alert(1)"} | ${'single quote'}
45
+ ${'foo<bar'} | ${'less than'}
46
+ ${'"><script>alert(1)</script>'} | ${'greater than'}
47
+ ${'foo bar'} | ${'space'}
48
+ ${'foo=bar'} | ${'equals sign'}
49
+ ${'foo/bar'} | ${'slash'}
50
+ ${'foo\\bar'} | ${'backslash'}
51
+ ${'foo`bar'} | ${'backtick'}
52
+ ${'a\x00b'} | ${'null byte'}
53
+ ${'a\x1fb'} | ${'control character'}
54
+ ${'a\x7fb'} | ${'DEL character'}
55
+ `('should return false for "$description"', ({ name }) => {
56
+ expect(isValidAttributeName(name)).toBe(false)
57
+ })
58
+
59
+ test.each`
60
+ name | description
61
+ ${'xlink:href'} | ${'namespace separator'}
62
+ ${'data.foo'} | ${'dot'}
63
+ ${'data_foo'} | ${'underscore'}
64
+ ${'é'} | ${'non-ascii'}
65
+ `('should return true for valid "$description" names', ({ name }) => {
66
+ expect(isValidAttributeName(name)).toBe(true)
67
+ })
68
+
69
+ it('should keep validating names after the valid attribute name cache is reset', () => {
70
+ for (let i = 0; i < 1025; i++) {
71
+ expect(isValidAttributeName(`data-k${i}`)).toBe(true)
72
+ }
73
+
74
+ expect(isValidAttributeName('class')).toBe(true)
75
+ expect(isValidAttributeName('" onfocus="alert(1)')).toBe(false)
76
+ })
77
+ })
78
+
79
+ describe('isValidTagName', () => {
80
+ test.each`
81
+ name
82
+ ${''}
83
+ ${'div'}
84
+ ${'h1'}
85
+ ${'custom-element'}
86
+ ${'clipPath'}
87
+ ${'foo:bar'}
88
+ ${'x.foo'}
89
+ ${'x_bar'}
90
+ ${'é'}
91
+ `('should return true for valid tag name "$name"', ({ name }) => {
92
+ expect(isValidTagName(name)).toBe(true)
93
+ })
94
+
95
+ test.each`
96
+ name | description
97
+ ${'foo bar'} | ${'space'}
98
+ ${'foo\nbar'} | ${'newline'}
99
+ ${'" onfocus="alert(1)'} | ${'double quote'}
100
+ ${"' onfocus='alert(1)"} | ${'single quote'}
101
+ ${'foo<bar'} | ${'less than'}
102
+ ${'"><script>alert(1)</script>'} | ${'greater than'}
103
+ ${'foo=bar'} | ${'equals sign'}
104
+ ${'foo/bar'} | ${'slash'}
105
+ ${'foo\\bar'} | ${'backslash'}
106
+ ${'foo`bar'} | ${'backtick'}
107
+ ${'a\x00b'} | ${'null byte'}
108
+ ${'a\x1fb'} | ${'control character'}
109
+ ${'a\x7fb'} | ${'DEL character'}
110
+ ${'!'} | ${'single bang'}
111
+ ${'!--'} | ${'parser-control bang prefix'}
112
+ ${'!DOCTYPE'} | ${'doctype-like name'}
113
+ ${'?'} | ${'single question mark'}
114
+ ${'?xml'} | ${'processing instruction'}
115
+ `('should return false for "$description"', ({ name }) => {
116
+ expect(isValidTagName(name)).toBe(false)
117
+ })
118
+
119
+ it('should keep validating names after the valid tag name cache is reset', () => {
120
+ for (let i = 0; i < 257; i++) {
121
+ expect(isValidTagName(`custom-${i}`)).toBe(true)
122
+ }
123
+
124
+ expect(isValidTagName('div')).toBe(true)
125
+ expect(isValidTagName('div onmouseover="alert(1)"')).toBe(false)
126
+ })
127
+ })
128
+
129
+ describe('styleObjectForEach', () => {
130
+ describe('Should output the number as it is, when a number type is passed', () => {
131
+ test.each`
132
+ property
133
+ ${'animationIterationCount'}
134
+ ${'aspectRatio'}
135
+ ${'borderImageOutset'}
136
+ ${'borderImageSlice'}
137
+ ${'borderImageWidth'}
138
+ ${'columnCount'}
139
+ ${'columns'}
140
+ ${'flex'}
141
+ ${'flexGrow'}
142
+ ${'flexPositive'}
143
+ ${'flexShrink'}
144
+ ${'flexNegative'}
145
+ ${'flexOrder'}
146
+ ${'gridArea'}
147
+ ${'gridRow'}
148
+ ${'gridRowEnd'}
149
+ ${'gridRowSpan'}
150
+ ${'gridRowStart'}
151
+ ${'gridColumn'}
152
+ ${'gridColumnEnd'}
153
+ ${'gridColumnSpan'}
154
+ ${'gridColumnStart'}
155
+ ${'fontWeight'}
156
+ ${'lineClamp'}
157
+ ${'lineHeight'}
158
+ ${'opacity'}
159
+ ${'order'}
160
+ ${'orphans'}
161
+ ${'scale'}
162
+ ${'tabSize'}
163
+ ${'widows'}
164
+ ${'zIndex'}
165
+ ${'zoom'}
166
+ ${'fillOpacity'}
167
+ ${'floodOpacity'}
168
+ ${'stopOpacity'}
169
+ ${'strokeDasharray'}
170
+ ${'strokeDashoffset'}
171
+ ${'strokeMiterlimit'}
172
+ ${'strokeOpacity'}
173
+ ${'strokeWidth'}
174
+ `('$property', ({ property }) => {
175
+ const fn = vi.fn()
176
+ styleObjectForEach({ [property]: 1 }, fn)
177
+ expect(fn).toBeCalledWith(
178
+ property.replace(/[A-Z]/g, (m: string) => `-${m.toLowerCase()}`),
179
+ '1'
180
+ )
181
+ })
182
+ })
183
+ describe('Should output with px suffix, when a number type is passed', () => {
184
+ test.each`
185
+ property
186
+ ${'borderBottomWidth'}
187
+ ${'borderLeftWidth'}
188
+ ${'borderRightWidth'}
189
+ ${'borderTopWidth'}
190
+ ${'borderWidth'}
191
+ ${'bottom'}
192
+ ${'fontSize'}
193
+ ${'height'}
194
+ ${'left'}
195
+ ${'margin'}
196
+ ${'marginBottom'}
197
+ ${'marginLeft'}
198
+ ${'marginRight'}
199
+ ${'marginTop'}
200
+ ${'padding'}
201
+ ${'paddingBottom'}
202
+ ${'paddingLeft'}
203
+ ${'paddingRight'}
204
+ ${'paddingTop'}
205
+ ${'right'}
206
+ ${'top'}
207
+ ${'width'}
208
+ ${'gridGap'}
209
+ ${'gridRowGap'}
210
+ ${'gridColumnGap'}
211
+ `('$property', ({ property }) => {
212
+ const fn = vi.fn()
213
+ styleObjectForEach({ [property]: 1 }, fn)
214
+ expect(fn).toBeCalledWith(
215
+ property.replace(/[A-Z]/g, (m: string) => `-${m.toLowerCase()}`),
216
+ '1px'
217
+ )
218
+ })
219
+ })
220
+
221
+ describe('Should skip values containing ";" to prevent CSS injection', () => {
222
+ it('skips a value with ";"', () => {
223
+ const fn = vi.fn()
224
+ styleObjectForEach({ color: 'red;background:blue' }, fn)
225
+ expect(fn).not.toBeCalled()
226
+ })
227
+
228
+ it('keeps safe siblings while skipping unsafe ones', () => {
229
+ const fn = vi.fn()
230
+ styleObjectForEach({ color: 'red;background:blue', backgroundColor: 'white' }, fn)
231
+ expect(fn).toBeCalledTimes(1)
232
+ expect(fn).toBeCalledWith('background-color', 'white')
233
+ })
234
+
235
+ it('does not throw for unsafe values', () => {
236
+ expect(() => styleObjectForEach({ color: 'a;b' }, () => {})).not.toThrow()
237
+ })
238
+
239
+ it('keeps semicolons inside quoted strings', () => {
240
+ const fn = vi.fn()
241
+ styleObjectForEach({ fontFamily: '"a;b", sans-serif' }, fn)
242
+ expect(fn).toBeCalledWith('font-family', '"a;b", sans-serif')
243
+ })
244
+
245
+ test.each([
246
+ ['LF', '\n'],
247
+ ['CR', '\r'],
248
+ ['FF', '\f'],
249
+ ])('skips quoted strings that contain %s before a declaration separator', (_, newline) => {
250
+ const fn = vi.fn()
251
+ styleObjectForEach(
252
+ { fontFamily: `"${newline};background:url(https://example.com/a.png)` },
253
+ fn
254
+ )
255
+ expect(fn).not.toBeCalled()
256
+ })
257
+
258
+ it('keeps semicolons inside CSS functions', () => {
259
+ const fn = vi.fn()
260
+ styleObjectForEach({ backgroundImage: 'url("data:image/svg+xml;utf8,<svg></svg>")' }, fn)
261
+ expect(fn).toBeCalledWith('background-image', 'url("data:image/svg+xml;utf8,<svg></svg>")')
262
+ })
263
+
264
+ test.each([['square brackets', 'red[;background:blue]']])(
265
+ 'keeps semicolons inside CSS simple blocks with %s',
266
+ (_, value) => {
267
+ const fn = vi.fn()
268
+ styleObjectForEach({ color: value }, fn)
269
+ expect(fn).toBeCalledWith('color', value)
270
+ }
271
+ )
272
+
273
+ test.each([
274
+ ['opening curly block', 'red{;background:blue}'],
275
+ ['closing curly block', 'red};background:blue'],
276
+ ])('skips CSS values containing %s', (_, value) => {
277
+ const fn = vi.fn()
278
+ styleObjectForEach({ color: value, backgroundColor: 'white' }, fn)
279
+ expect(fn).toBeCalledTimes(1)
280
+ expect(fn).toBeCalledWith('background-color', 'white')
281
+ })
282
+
283
+ test.each([
284
+ ['square brackets', 'red[;background:blue];position:fixed'],
285
+ ['curly braces', 'red{;background:blue};position:fixed'],
286
+ ])('skips top-level semicolons after CSS simple blocks with %s', (_, value) => {
287
+ const fn = vi.fn()
288
+ styleObjectForEach({ color: value }, fn)
289
+ expect(fn).not.toBeCalled()
290
+ })
291
+
292
+ it('skips top-level semicolons after CSS comments', () => {
293
+ const fn = vi.fn()
294
+ styleObjectForEach({ color: 'red/*(*/;background:blue;position:fixed;top:0' }, fn)
295
+ expect(fn).not.toBeCalled()
296
+ })
297
+
298
+ test.each([
299
+ ['comment', 'red/*'],
300
+ ['double-quoted string', '"abc'],
301
+ ['single-quoted string', "'abc"],
302
+ ['function block', 'red('],
303
+ ['square block', 'red['],
304
+ ['curly block', 'red{'],
305
+ ['unmatched function closer', 'red)'],
306
+ ['unmatched square closer', 'red]'],
307
+ ['unmatched curly closer', 'red}'],
308
+ ['mismatched simple block closer', 'red[)'],
309
+ ['escape', 'red\\'],
310
+ ])('skips unterminated CSS %s that can swallow following declarations', (_, value) => {
311
+ const fn = vi.fn()
312
+ styleObjectForEach({ color: value, display: 'none' }, fn)
313
+ expect(fn).toBeCalledTimes(1)
314
+ expect(fn).toBeCalledWith('display', 'none')
315
+ })
316
+
317
+ test.each([
318
+ ['comment that closes exactly at end of value', 'red/* hi */'],
319
+ ['CSS hex escape that decodes to a delimiter byte', 'red\\3b '],
320
+ ['vertical tab inside a quoted string', '"\vfoo"'],
321
+ ['trailing solidus that is not a comment start', 'a/'],
322
+ ])('keeps CSS-safe edge case — %s', (_, value) => {
323
+ const fn = vi.fn()
324
+ styleObjectForEach({ color: value }, fn)
325
+ expect(fn).toBeCalledWith('color', value)
326
+ })
327
+
328
+ it('skips style property names that can break declaration boundaries', () => {
329
+ const fn = vi.fn()
330
+ styleObjectForEach({ 'color;background-image': 'url(https://example.com/a.png)' }, fn)
331
+ styleObjectForEach({ 'color:background': 'red' }, fn)
332
+ expect(fn).not.toBeCalled()
333
+ })
334
+
335
+ it('keeps safe property names that use digits', () => {
336
+ const fn = vi.fn()
337
+ styleObjectForEach({ '--my-var-1': '15px', '--myVar-2': '20px' }, fn)
338
+ expect(fn).toHaveBeenNthCalledWith(1, '--my-var-1', '15px')
339
+ expect(fn).toHaveBeenNthCalledWith(2, '--myVar-2', '20px')
340
+ })
341
+
342
+ it('keeps vendor-prefixed property names', () => {
343
+ const fn = vi.fn()
344
+ styleObjectForEach({ WebkitLineClamp: '2' }, fn)
345
+ expect(fn).toBeCalledWith('-webkit-line-clamp', '2')
346
+ })
347
+
348
+ it('keeps validating style property names after the valid style property name cache is reset', () => {
349
+ for (let i = 0; i < 1025; i++) {
350
+ styleObjectForEach({ [`--cache-${i}`]: '1px' }, () => {})
351
+ }
352
+
353
+ const fn = vi.fn()
354
+ styleObjectForEach({ '--after-reset-1': '2px', 'color:background': 'red' }, fn)
355
+ expect(fn).toBeCalledTimes(1)
356
+ expect(fn).toBeCalledWith('--after-reset-1', '2px')
357
+ })
358
+
359
+ it('skips unsupported runtime values without throwing', () => {
360
+ const fn = vi.fn()
361
+ expect(() => styleObjectForEach({ color: false, backgroundColor: 'white' }, fn)).not.toThrow()
362
+ expect(fn).toBeCalledTimes(1)
363
+ expect(fn).toBeCalledWith('background-color', 'white')
364
+ })
365
+ })
366
+ })