@typed/template 0.2.0 → 0.3.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 (213) hide show
  1. package/dist/cjs/Directive.js +1 -1
  2. package/dist/cjs/Directive.js.map +1 -1
  3. package/dist/cjs/ElementRef.js +23 -13
  4. package/dist/cjs/ElementRef.js.map +1 -1
  5. package/dist/cjs/ElementSource.js +16 -18
  6. package/dist/cjs/ElementSource.js.map +1 -1
  7. package/dist/cjs/EventHandler.js +1 -1
  8. package/dist/cjs/EventHandler.js.map +1 -1
  9. package/dist/cjs/Html.js +31 -32
  10. package/dist/cjs/Html.js.map +1 -1
  11. package/dist/cjs/HtmlChunk.js +4 -1
  12. package/dist/cjs/HtmlChunk.js.map +1 -1
  13. package/dist/cjs/Hydrate.js +1 -1
  14. package/dist/cjs/Hydrate.js.map +1 -1
  15. package/dist/cjs/Many.js +14 -13
  16. package/dist/cjs/Many.js.map +1 -1
  17. package/dist/cjs/Parser.js +11 -323
  18. package/dist/cjs/Parser.js.map +1 -1
  19. package/dist/cjs/Placeholder.js +3 -3
  20. package/dist/cjs/Placeholder.js.map +1 -1
  21. package/dist/cjs/Platform.js +4 -4
  22. package/dist/cjs/Platform.js.map +1 -1
  23. package/dist/cjs/Render.js +1 -1
  24. package/dist/cjs/Render.js.map +1 -1
  25. package/dist/cjs/RenderContext.js +48 -27
  26. package/dist/cjs/RenderContext.js.map +1 -1
  27. package/dist/cjs/RenderTemplate.js +2 -17
  28. package/dist/cjs/RenderTemplate.js.map +1 -1
  29. package/dist/cjs/Template.js +27 -1
  30. package/dist/cjs/Template.js.map +1 -1
  31. package/dist/cjs/TemplateInstance.js +2 -2
  32. package/dist/cjs/TemplateInstance.js.map +1 -1
  33. package/dist/cjs/Test.js +20 -7
  34. package/dist/cjs/Test.js.map +1 -1
  35. package/dist/cjs/index.js +0 -12
  36. package/dist/cjs/index.js.map +1 -1
  37. package/dist/cjs/internal/EventSource.js +95 -0
  38. package/dist/cjs/internal/EventSource.js.map +1 -0
  39. package/dist/cjs/internal/browser.js +11 -11
  40. package/dist/cjs/internal/browser.js.map +1 -1
  41. package/dist/cjs/internal/hydrate.js +49 -50
  42. package/dist/cjs/internal/hydrate.js.map +1 -1
  43. package/dist/cjs/internal/indexRefCounter.js +49 -2
  44. package/dist/cjs/internal/indexRefCounter.js.map +1 -1
  45. package/dist/cjs/internal/parser.js +60 -17
  46. package/dist/cjs/internal/parser.js.map +1 -1
  47. package/dist/cjs/internal/parts.js +128 -28
  48. package/dist/cjs/internal/parts.js.map +1 -1
  49. package/dist/cjs/internal/render.js +486 -53
  50. package/dist/cjs/internal/render.js.map +1 -1
  51. package/dist/cjs/internal/server.js +5 -2
  52. package/dist/cjs/internal/server.js.map +1 -1
  53. package/dist/dts/Directive.d.ts.map +1 -1
  54. package/dist/dts/ElementRef.d.ts +4 -2
  55. package/dist/dts/ElementRef.d.ts.map +1 -1
  56. package/dist/dts/ElementSource.d.ts +10 -5
  57. package/dist/dts/ElementSource.d.ts.map +1 -1
  58. package/dist/dts/EventHandler.d.ts.map +1 -1
  59. package/dist/dts/Html.d.ts +1 -1
  60. package/dist/dts/Html.d.ts.map +1 -1
  61. package/dist/dts/HtmlChunk.d.ts.map +1 -1
  62. package/dist/dts/Many.d.ts +13 -11
  63. package/dist/dts/Many.d.ts.map +1 -1
  64. package/dist/dts/Parser.d.ts +3 -6
  65. package/dist/dts/Parser.d.ts.map +1 -1
  66. package/dist/dts/Part.d.ts +13 -3
  67. package/dist/dts/Part.d.ts.map +1 -1
  68. package/dist/dts/Placeholder.d.ts +2 -2
  69. package/dist/dts/Placeholder.d.ts.map +1 -1
  70. package/dist/dts/Render.d.ts +2 -1
  71. package/dist/dts/Render.d.ts.map +1 -1
  72. package/dist/dts/RenderContext.d.ts +5 -4
  73. package/dist/dts/RenderContext.d.ts.map +1 -1
  74. package/dist/dts/RenderTemplate.d.ts +2 -16
  75. package/dist/dts/RenderTemplate.d.ts.map +1 -1
  76. package/dist/dts/Renderable.d.ts +2 -2
  77. package/dist/dts/Renderable.d.ts.map +1 -1
  78. package/dist/dts/Template.d.ts +21 -3
  79. package/dist/dts/Template.d.ts.map +1 -1
  80. package/dist/dts/TemplateInstance.d.ts +3 -2
  81. package/dist/dts/TemplateInstance.d.ts.map +1 -1
  82. package/dist/dts/Test.d.ts +4 -6
  83. package/dist/dts/Test.d.ts.map +1 -1
  84. package/dist/dts/index.d.ts +0 -4
  85. package/dist/dts/index.d.ts.map +1 -1
  86. package/dist/dts/internal/EventSource.d.ts +12 -0
  87. package/dist/dts/internal/EventSource.d.ts.map +1 -0
  88. package/dist/dts/internal/browser.d.ts.map +1 -1
  89. package/dist/dts/internal/hydrate.d.ts +5 -5
  90. package/dist/dts/internal/hydrate.d.ts.map +1 -1
  91. package/dist/dts/internal/indexRefCounter.d.ts +5 -0
  92. package/dist/dts/internal/indexRefCounter.d.ts.map +1 -1
  93. package/dist/dts/internal/module-augmentation.d.ts +0 -4
  94. package/dist/dts/internal/module-augmentation.d.ts.map +1 -1
  95. package/dist/dts/internal/parser.d.ts +8 -0
  96. package/dist/dts/internal/parser.d.ts.map +1 -1
  97. package/dist/dts/internal/parts.d.ts +66 -56
  98. package/dist/dts/internal/parts.d.ts.map +1 -1
  99. package/dist/dts/internal/render.d.ts +7 -7
  100. package/dist/dts/internal/render.d.ts.map +1 -1
  101. package/dist/dts/internal/server.d.ts.map +1 -1
  102. package/dist/esm/Directive.js +1 -1
  103. package/dist/esm/Directive.js.map +1 -1
  104. package/dist/esm/ElementRef.js +12 -7
  105. package/dist/esm/ElementRef.js.map +1 -1
  106. package/dist/esm/ElementSource.js +16 -13
  107. package/dist/esm/ElementSource.js.map +1 -1
  108. package/dist/esm/EventHandler.js +1 -1
  109. package/dist/esm/EventHandler.js.map +1 -1
  110. package/dist/esm/Html.js +29 -24
  111. package/dist/esm/Html.js.map +1 -1
  112. package/dist/esm/HtmlChunk.js +6 -1
  113. package/dist/esm/HtmlChunk.js.map +1 -1
  114. package/dist/esm/Hydrate.js +1 -1
  115. package/dist/esm/Hydrate.js.map +1 -1
  116. package/dist/esm/Many.js +14 -10
  117. package/dist/esm/Many.js.map +1 -1
  118. package/dist/esm/Parser.js +6 -335
  119. package/dist/esm/Parser.js.map +1 -1
  120. package/dist/esm/Placeholder.js +2 -2
  121. package/dist/esm/Placeholder.js.map +1 -1
  122. package/dist/esm/Platform.js +2 -2
  123. package/dist/esm/Platform.js.map +1 -1
  124. package/dist/esm/Render.js +1 -1
  125. package/dist/esm/Render.js.map +1 -1
  126. package/dist/esm/RenderContext.js +38 -17
  127. package/dist/esm/RenderContext.js.map +1 -1
  128. package/dist/esm/RenderTemplate.js +2 -12
  129. package/dist/esm/RenderTemplate.js.map +1 -1
  130. package/dist/esm/Template.js +24 -0
  131. package/dist/esm/Template.js.map +1 -1
  132. package/dist/esm/TemplateInstance.js +2 -2
  133. package/dist/esm/TemplateInstance.js.map +1 -1
  134. package/dist/esm/Test.js +20 -7
  135. package/dist/esm/Test.js.map +1 -1
  136. package/dist/esm/index.js +0 -4
  137. package/dist/esm/index.js.map +1 -1
  138. package/dist/esm/internal/EventSource.js +91 -0
  139. package/dist/esm/internal/EventSource.js.map +1 -0
  140. package/dist/esm/internal/browser.js +12 -12
  141. package/dist/esm/internal/browser.js.map +1 -1
  142. package/dist/esm/internal/hydrate.js +45 -46
  143. package/dist/esm/internal/hydrate.js.map +1 -1
  144. package/dist/esm/internal/indexRefCounter.js +50 -2
  145. package/dist/esm/internal/indexRefCounter.js.map +1 -1
  146. package/dist/esm/internal/parser.js +84 -17
  147. package/dist/esm/internal/parser.js.map +1 -1
  148. package/dist/esm/internal/parts.js +110 -27
  149. package/dist/esm/internal/parts.js.map +1 -1
  150. package/dist/esm/internal/render.js +476 -58
  151. package/dist/esm/internal/render.js.map +1 -1
  152. package/dist/esm/internal/server.js +5 -4
  153. package/dist/esm/internal/server.js.map +1 -1
  154. package/package.json +10 -26
  155. package/src/Directive.ts +1 -1
  156. package/src/ElementRef.ts +18 -14
  157. package/src/ElementSource.ts +62 -47
  158. package/src/EventHandler.ts +1 -1
  159. package/src/Html.ts +58 -57
  160. package/src/HtmlChunk.ts +15 -1
  161. package/src/Hydrate.ts +1 -1
  162. package/src/Many.ts +53 -43
  163. package/src/Parser.ts +10 -453
  164. package/src/Part.ts +15 -3
  165. package/src/Placeholder.ts +4 -4
  166. package/src/Platform.ts +2 -2
  167. package/src/Render.ts +7 -2
  168. package/src/RenderContext.ts +47 -19
  169. package/src/RenderTemplate.ts +9 -54
  170. package/src/Renderable.ts +2 -1
  171. package/src/Template.ts +26 -0
  172. package/src/TemplateInstance.ts +9 -9
  173. package/src/Test.ts +40 -21
  174. package/src/index.ts +0 -4
  175. package/src/internal/EventSource.ts +153 -0
  176. package/src/internal/browser.ts +26 -25
  177. package/src/internal/hydrate.ts +68 -61
  178. package/src/internal/indexRefCounter.ts +63 -2
  179. package/src/internal/module-augmentation.ts +0 -4
  180. package/src/internal/parser.ts +92 -19
  181. package/src/internal/parts.ts +158 -73
  182. package/src/internal/render.ts +701 -89
  183. package/src/internal/server.ts +5 -3
  184. package/Token/package.json +0 -6
  185. package/Tokenizer/package.json +0 -6
  186. package/dist/cjs/Token.js +0 -270
  187. package/dist/cjs/Token.js.map +0 -1
  188. package/dist/cjs/Tokenizer.js +0 -18
  189. package/dist/cjs/Tokenizer.js.map +0 -1
  190. package/dist/cjs/internal/readAttribute.js +0 -34
  191. package/dist/cjs/internal/readAttribute.js.map +0 -1
  192. package/dist/cjs/internal/tokenizer.js +0 -264
  193. package/dist/cjs/internal/tokenizer.js.map +0 -1
  194. package/dist/dts/Token.d.ts +0 -202
  195. package/dist/dts/Token.d.ts.map +0 -1
  196. package/dist/dts/Tokenizer.d.ts +0 -6
  197. package/dist/dts/Tokenizer.d.ts.map +0 -1
  198. package/dist/dts/internal/readAttribute.d.ts +0 -9
  199. package/dist/dts/internal/readAttribute.d.ts.map +0 -1
  200. package/dist/dts/internal/tokenizer.d.ts +0 -3
  201. package/dist/dts/internal/tokenizer.d.ts.map +0 -1
  202. package/dist/esm/Token.js +0 -264
  203. package/dist/esm/Token.js.map +0 -1
  204. package/dist/esm/Tokenizer.js +0 -9
  205. package/dist/esm/Tokenizer.js.map +0 -1
  206. package/dist/esm/internal/readAttribute.js +0 -24
  207. package/dist/esm/internal/readAttribute.js.map +0 -1
  208. package/dist/esm/internal/tokenizer.js +0 -296
  209. package/dist/esm/internal/tokenizer.js.map +0 -1
  210. package/src/Token.ts +0 -269
  211. package/src/Tokenizer.ts +0 -10
  212. package/src/internal/readAttribute.ts +0 -28
  213. package/src/internal/tokenizer.ts +0 -338
@@ -1,17 +1,28 @@
1
1
  import * as Fx from "@typed/fx/Fx"
2
- import * as Subject from "@typed/fx/Subject"
3
2
  import { TypeId } from "@typed/fx/TypeId"
4
3
  import type { Rendered } from "@typed/wire"
5
4
  import { persistent } from "@typed/wire"
6
5
  import { Effect } from "effect"
7
6
  import type { Cause } from "effect/Cause"
7
+ import * as Context from "effect/Context"
8
8
  import { replace } from "effect/ReadonlyArray"
9
- import type { Scope } from "effect/Scope"
9
+ import { Scope } from "effect/Scope"
10
+ import type { Directive } from "../Directive.js"
10
11
  import { isDirective } from "../Directive.js"
11
12
  import * as ElementRef from "../ElementRef.js"
13
+ import * as ElementSource from "../ElementSource.js"
12
14
  import type { BrowserEntry } from "../Entry.js"
13
15
  import * as EventHandler from "../EventHandler.js"
14
- import type { AttributePart, ClassNamePart, CommentPart, Part, Parts, SparsePart, StaticText } from "../Part.js"
16
+ import type {
17
+ AttributePart,
18
+ ClassNamePart,
19
+ CommentPart,
20
+ Part,
21
+ Parts,
22
+ PropertiesPart,
23
+ SparsePart,
24
+ StaticText
25
+ } from "../Part.js"
15
26
  import type { Placeholder } from "../Placeholder.js"
16
27
  import type { ToRendered } from "../Render.js"
17
28
  import type { Renderable } from "../Renderable.js"
@@ -20,11 +31,11 @@ import type { RenderEvent } from "../RenderEvent.js"
20
31
  import { DomRenderEvent } from "../RenderEvent.js"
21
32
  import type { RenderTemplate } from "../RenderTemplate.js"
22
33
  import type * as Template from "../Template.js"
23
- import { TemplateInstance } from "../TemplateInstance.js"
24
34
  import { makeRenderNodePart } from "./browser.js"
35
+ import { type EventSource, makeEventSource } from "./EventSource.js"
25
36
  import { HydrateContext } from "./HydrateContext.js"
26
- import type { IndexRefCounter } from "./indexRefCounter.js"
27
- import { indexRefCounter } from "./indexRefCounter.js"
37
+ import type { IndexRefCounter, IndexRefCounter2 } from "./indexRefCounter.js"
38
+ import { indexRefCounter2 } from "./indexRefCounter.js"
28
39
  import { parse } from "./parser.js"
29
40
  import {
30
41
  AttributePartImpl,
@@ -33,6 +44,7 @@ import {
33
44
  CommentPartImpl,
34
45
  DataPartImpl,
35
46
  EventPartImpl,
47
+ PropertiesPartImpl,
36
48
  PropertyPartImpl,
37
49
  RefPartImpl,
38
50
  SparseAttributePartImpl,
@@ -42,51 +54,547 @@ import {
42
54
  TextPartImpl
43
55
  } from "./parts.js"
44
56
  import type { ParentChildNodes } from "./utils.js"
45
- import { findPath } from "./utils.js"
57
+ import { findHoleComment, findPath } from "./utils.js"
58
+
59
+ // TODO: We need to add support for hydration of templates
60
+ // TODO: We need to re-think hydration for dynamic lists, probably just markers should be fine
61
+ // TODO: We need to make Parts synchronous
62
+
63
+ /**
64
+ * @internal
65
+ */
66
+ type RenderPartContext = {
67
+ readonly context: Context.Context<Scope>
68
+ readonly document: Document
69
+ readonly eventSource: EventSource
70
+ readonly refCounter: IndexRefCounter2
71
+ readonly renderContext: RenderContext
72
+ readonly values: ReadonlyArray<Renderable<any, any>>
73
+ readonly onCause: (cause: Cause<unknown>) => Effect.Effect<never, never, void>
74
+
75
+ expected: number
76
+ }
77
+
78
+ type RenderPartMap = {
79
+ readonly [K in Template.PartNode["_tag"] | Template.SparsePartNode["_tag"]]: (
80
+ part: Extract<Template.PartNode | Template.SparsePartNode, { _tag: K }>,
81
+ node: Node,
82
+ ctx: RenderPartContext
83
+ ) => null | Effect.Effect<any, any, void> | Array<Effect.Effect<any, any, void>>
84
+ }
85
+
86
+ const RenderPartMap: RenderPartMap = {
87
+ "attr": (templatePart, node, ctx) => {
88
+ const { document, refCounter, renderContext, values } = ctx
89
+ const element = node as HTMLElement | SVGElement
90
+ const attr = createAttribute(document, element, templatePart.name)
91
+ const renderable = values[templatePart.index]
92
+ let isSet = true
93
+ const setValue = (value: string | null | undefined) => {
94
+ if (isNullOrUndefined(value)) {
95
+ element.removeAttribute(templatePart.name)
96
+ isSet = false
97
+ } else {
98
+ attr.value = String(value)
99
+ if (isSet === false) {
100
+ element.setAttributeNode(attr)
101
+ isSet = true
102
+ }
103
+ }
104
+ }
105
+
106
+ return matchSettablePart(
107
+ renderable,
108
+ setValue,
109
+ () => AttributePartImpl.browser(templatePart.index, element, templatePart.name, renderContext),
110
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
111
+ () => ctx.expected++
112
+ )
113
+ },
114
+ "boolean-part": (templatePart, node, ctx) => {
115
+ const { refCounter, renderContext, values } = ctx
116
+ const element = node as HTMLElement | SVGElement
117
+ const renderable = values[templatePart.index]
118
+ const setValue = (value: boolean | null | undefined) => {
119
+ element.toggleAttribute(templatePart.name, isNullOrUndefined(value) ? false : Boolean(value))
120
+ }
121
+
122
+ return matchSettablePart(
123
+ renderable,
124
+ setValue,
125
+ () => BooleanPartImpl.browser(templatePart.index, element, templatePart.name, renderContext),
126
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
127
+ () => ctx.expected++
128
+ )
129
+ },
130
+ "className-part": (templatePart, node, ctx) => {
131
+ const { refCounter, renderContext, values } = ctx
132
+ const element = node as HTMLElement | SVGElement
133
+ const renderable = values[templatePart.index]
134
+ let classNames: Set<string> = new Set()
135
+ const setValue = (value: string | Array<string> | null | undefined) => {
136
+ if (isNullOrUndefined(value)) {
137
+ element.classList.remove(...classNames)
138
+ classNames.clear()
139
+ } else {
140
+ const newClassNames = new Set(Array.isArray(value) ? value : [String(value)])
141
+ const { added, removed } = diffClassNames(classNames, newClassNames)
142
+
143
+ if (removed.length > 0) {
144
+ element.classList.remove(...removed)
145
+ }
146
+ if (added.length > 0) element.classList.add(...added)
147
+
148
+ classNames = newClassNames
149
+ }
150
+ }
151
+
152
+ return matchSettablePart(
153
+ renderable,
154
+ setValue,
155
+ () => ClassNamePartImpl.browser(templatePart.index, element, renderContext),
156
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
157
+ () => ctx.expected++
158
+ )
159
+ },
160
+ "comment-part": (templatePart, node, ctx) => {
161
+ const { refCounter, renderContext, values } = ctx
162
+ const comment = findHoleComment(node as Element, templatePart.index)
163
+ const renderable = values[templatePart.index]
164
+ const setValue = (value: string | null | undefined) => {
165
+ comment.textContent = isNullOrUndefined(value) ? "" : String(value)
166
+ }
167
+
168
+ return matchSettablePart(
169
+ renderable,
170
+ setValue,
171
+ () => CommentPartImpl.browser(templatePart.index, comment, renderContext),
172
+ (f) => Effect.zipRight(renderContext.queue.add(comment, f), refCounter.release(templatePart.index)),
173
+ () => ctx.expected++
174
+ )
175
+ },
176
+ "data": (templatePart, node, ctx) => {
177
+ const element = node as HTMLElement | SVGElement
178
+ const renderable = ctx.values[templatePart.index]
179
+ const previousKeys = new Set<string>(Object.keys(element.dataset))
180
+ const setValue = (value: Record<string, string | undefined> | null | undefined) => {
181
+ if (isNullOrUndefined(value)) {
182
+ for (const key of previousKeys) {
183
+ delete element.dataset[key]
184
+ }
185
+ previousKeys.clear()
186
+ } else {
187
+ for (const key of previousKeys) {
188
+ if (!(key in value)) {
189
+ delete element.dataset[key]
190
+ previousKeys.delete(key)
191
+ }
192
+ }
193
+
194
+ for (const key of Object.keys(value)) {
195
+ if (!previousKeys.has(key)) {
196
+ previousKeys.add(key)
197
+ }
198
+ element.dataset[key] = value[key] || ""
199
+ }
200
+ }
201
+ }
202
+
203
+ return matchSettablePart(
204
+ renderable,
205
+ setValue,
206
+ () => DataPartImpl.browser(templatePart.index, element, ctx.renderContext),
207
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(templatePart.index)),
208
+ () => ctx.expected++
209
+ )
210
+ },
211
+ "event": (templatePart, node, ctx) => {
212
+ const element = node as HTMLElement | SVGElement
213
+ const renderable = ctx.values[templatePart.index]
214
+ const handler = getEventHandler(renderable, ctx.context, ctx.onCause)
215
+ if (handler) {
216
+ ctx.eventSource.addEventListener(element, templatePart.name, handler)
217
+ }
218
+
219
+ return null
220
+ },
221
+ "node": (templatePart, node, ctx) => {
222
+ const part = makeRenderNodePart(
223
+ templatePart.index,
224
+ node as HTMLElement | SVGElement,
225
+ ctx.renderContext,
226
+ ctx.document,
227
+ false
228
+ )
229
+
230
+ ctx.expected++
231
+
232
+ return handlePart(
233
+ ctx.values[templatePart.index],
234
+ (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
235
+ )
236
+ },
237
+ "property": (templatePart, node, ctx) => {
238
+ const element = node as HTMLElement | SVGElement
239
+ const renderable = ctx.values[templatePart.index]
240
+ const setValue = (value: string | null | undefined) => {
241
+ if (isNullOrUndefined(value)) {
242
+ delete (element as any)[templatePart.name]
243
+ } else {
244
+ ;(element as any)[templatePart.name] = value
245
+ }
246
+ }
247
+
248
+ return matchSettablePart(
249
+ renderable,
250
+ setValue,
251
+ () => PropertyPartImpl.browser(templatePart.index, element, templatePart.name, ctx.renderContext),
252
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(templatePart.index)),
253
+ () => ctx.expected++
254
+ )
255
+ },
256
+ "properties": (templatePart, node, ctx) => {
257
+ const renderable = ctx.values[templatePart.index] as any as Record<string, any>
258
+
259
+ if (isNullOrUndefined(renderable)) return null
260
+ else if (Fx.isFx(renderable) || Effect.isEffect(renderable)) {
261
+ throw new Error(`Properties Part must utilize an Record of renderable values.`)
262
+ } else if (typeof renderable === "object" && !Array.isArray(renderable)) {
263
+ const element = node as HTMLElement | SVGElement
264
+
265
+ const toggleBoolean = (key: string, value: unknown) => {
266
+ element.toggleAttribute(key, isNullOrUndefined(value) ? false : Boolean(value))
267
+ }
268
+ const setAttribute = (key: string, value: unknown) => {
269
+ if (isNullOrUndefined(value)) {
270
+ element.removeAttribute(key)
271
+ } else {
272
+ element.setAttribute(key, String(value))
273
+ }
274
+ }
275
+ const setProperty = (key: string, value: unknown) => {
276
+ if (isNullOrUndefined(value)) {
277
+ delete (element as any)[key]
278
+ } else {
279
+ ;(element as any)[key] = value
280
+ }
281
+ }
282
+
283
+ const effects: Array<Effect.Effect<any, any, void>> = []
284
+
285
+ // We need indexes to track async values that won't conflict
286
+ // with any other Parts, we can start end of the current values.length
287
+ // As there should only ever be exactly 1 properties part.
288
+ let i = ctx.values.length
289
+
290
+ loop:
291
+ for (const [key, value] of Object.entries(renderable)) {
292
+ const index = ++i
293
+
294
+ switch (key[0]) {
295
+ case "?": {
296
+ const name = key.slice(1)
297
+ const eff = matchSettablePart(
298
+ value,
299
+ (value) => toggleBoolean(name, value),
300
+ () => BooleanPartImpl.browser(index, element, name, ctx.renderContext),
301
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
302
+ () => ctx.expected++
303
+ )
304
+ if (eff !== null) {
305
+ effects.push(eff)
306
+ }
307
+ continue loop
308
+ }
309
+ case ".": {
310
+ const name = key.slice(1)
311
+ const eff = matchSettablePart(
312
+ value,
313
+ (value) => setProperty(name, value),
314
+ () => PropertyPartImpl.browser(index, element, name, ctx.renderContext),
315
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
316
+ () => ctx.expected++
317
+ )
318
+ if (eff !== null) {
319
+ effects.push(eff)
320
+ }
321
+ continue loop
322
+ }
323
+ case "@": {
324
+ const name = key.slice(1)
325
+ const handler = getEventHandler(value, ctx.context, ctx.onCause)
326
+ if (handler) {
327
+ ctx.eventSource.addEventListener(element, name, handler)
328
+ }
329
+ continue loop
330
+ }
331
+ case "o": {
332
+ if (key[1] === "n") {
333
+ const name = key.slice(2)
334
+ const handler = getEventHandler(value, ctx.context, ctx.onCause)
335
+ if (handler) {
336
+ ctx.eventSource.addEventListener(element, name, handler)
337
+ }
338
+ }
339
+ continue loop
340
+ }
341
+ }
342
+
343
+ const eff = matchSettablePart(
344
+ value,
345
+ (value) => setAttribute(key, value),
346
+ () => AttributePartImpl.browser(index, element, key, ctx.renderContext),
347
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
348
+ () => ctx.expected++
349
+ )
350
+ if (eff !== null) {
351
+ effects.push(eff)
352
+ }
353
+ }
354
+
355
+ return effects
356
+ } else {
357
+ return null
358
+ }
359
+ },
360
+ "ref": (templatePart, node, ctx) => {
361
+ const element = node as HTMLElement | SVGElement
362
+ const renderable = ctx.values[templatePart.index]
363
+
364
+ if (isDirective(renderable)) {
365
+ return renderable(new RefPartImpl(ElementSource.fromElement(element), templatePart.index))
366
+ } else if (ElementRef.isElementRef(renderable)) {
367
+ return ElementRef.set(renderable, element)
368
+ }
369
+
370
+ return null
371
+ },
372
+ "sparse-attr": (templatePart, node, ctx) => {
373
+ const values = Array.from({ length: templatePart.nodes.length }, (): string => "")
374
+ const element = node as HTMLElement | SVGElement
375
+ const attr = createAttribute(ctx.document, element, templatePart.name)
376
+
377
+ const setValue = (value: string | null | undefined, index: number) =>
378
+ Effect.suspend(() => {
379
+ values[index] = value || ""
380
+ return ctx.renderContext.queue.add(element, () => attr.value = values.join(""))
381
+ })
382
+
383
+ const effects: Array<Effect.Effect<any, any, void>> = []
384
+
385
+ for (let i = 0; i < templatePart.nodes.length; ++i) {
386
+ const node = templatePart.nodes[i]
387
+ if (node._tag === "text") {
388
+ values[i] = node.value
389
+ } else {
390
+ const renderable = ctx.values[node.index]
391
+ const index = i
392
+ const effect = matchSettablePart(
393
+ renderable,
394
+ (value) => setValue(value, index),
395
+ () =>
396
+ new AttributePartImpl(
397
+ templatePart.name,
398
+ node.index,
399
+ ({ value }) => setValue(value, index),
400
+ null
401
+ ),
402
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(node.index)),
403
+ () => ctx.expected++
404
+ )
405
+
406
+ if (effect !== null) {
407
+ effects.push(effect)
408
+ }
409
+ }
410
+ }
411
+
412
+ return effects
413
+ },
414
+ "sparse-class-name": (templatePart, node, ctx) => {
415
+ const element = node as HTMLElement | SVGElement
416
+
417
+ const effects = templatePart.nodes.flatMap((node) => {
418
+ if (node._tag === "text") {
419
+ const split = splitClassNames(node.value)
420
+ if (split.length > 0) element.classList.add(...split)
421
+ return []
422
+ } else {
423
+ const eff = RenderPartMap[node._tag](node, element, ctx)
424
+ if (eff === null) return []
425
+ return Array.isArray(eff) ? eff : [eff]
426
+ }
427
+ })
428
+
429
+ return effects
430
+ },
431
+ "sparse-comment": (templatePart, node, ctx) => {
432
+ const values = Array.from({ length: templatePart.nodes.length }, (): string => "")
433
+ const comment = node as Comment
434
+
435
+ const setValue = (value: string | null | undefined, index: number) =>
436
+ Effect.suspend(() => {
437
+ values[index] = value || ""
438
+ return ctx.renderContext.queue.add(comment, () => comment.textContent = values.join(""))
439
+ })
440
+
441
+ const effects: Array<Effect.Effect<any, any, void>> = []
442
+
443
+ for (let i = 0; i < templatePart.nodes.length; ++i) {
444
+ const node = templatePart.nodes[i]
445
+ if (node._tag === "text") {
446
+ values[i] = node.value
447
+ } else {
448
+ const renderable = ctx.values[node.index]
449
+ const index = i
450
+ const effect = matchSettablePart(
451
+ renderable,
452
+ (value) => setValue(value, index),
453
+ () =>
454
+ new CommentPartImpl(
455
+ node.index,
456
+ ({ value }) => setValue(value, index),
457
+ null
458
+ ),
459
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(comment, f), ctx.refCounter.release(node.index)),
460
+ () => ctx.expected++
461
+ )
462
+
463
+ if (effect !== null) {
464
+ effects.push(effect)
465
+ }
466
+ }
467
+ }
468
+
469
+ return effects
470
+ },
471
+ "text-part": (templatePart, node, ctx) => {
472
+ const part = TextPartImpl.browser(
473
+ ctx.document,
474
+ templatePart.index,
475
+ node as HTMLElement | SVGElement,
476
+ ctx.renderContext
477
+ )
478
+
479
+ ctx.expected++
480
+
481
+ return handlePart(
482
+ ctx.values[templatePart.index],
483
+ (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
484
+ )
485
+ }
486
+ }
487
+
488
+ const SPACE_REGEXP = /\s+/g
489
+
490
+ function splitClassNames(value: string) {
491
+ return value.split(SPACE_REGEXP).flatMap((a) => {
492
+ const trimmed = a.trim()
493
+ return trimmed.length > 0 ? [trimmed] : []
494
+ })
495
+ }
496
+
497
+ function isNullOrUndefined<T>(value: T | null | undefined): value is null | undefined {
498
+ return value === null || value === undefined
499
+ }
500
+
501
+ function diffClassNames(oldClassNames: Set<string>, newClassNames: Set<string>) {
502
+ const added: Array<string> = []
503
+ const removed: Array<string> = []
504
+
505
+ for (const className of oldClassNames) {
506
+ if (!newClassNames.has(className)) {
507
+ removed.push(className)
508
+ }
509
+ }
510
+
511
+ for (const className of newClassNames) {
512
+ if (!oldClassNames.has(className) && className.trim()) {
513
+ added.push(className)
514
+ }
515
+ }
516
+
517
+ return { added, removed }
518
+ }
46
519
 
47
520
  /**
48
521
  * Here for "standard" browser rendering, a TemplateInstance is effectively a live
49
522
  * view into the contents rendered by the Template.
50
523
  */
51
- export const renderTemplate: (document: Document, ctx: RenderContext) => RenderTemplate =
52
- (document, ctx) =>
524
+ export const renderTemplate: (document: Document, renderContext: RenderContext) => RenderTemplate =
525
+ (document, renderContext) =>
53
526
  <Values extends ReadonlyArray<Renderable<any, any>>, T extends Rendered = Rendered>(
54
527
  templateStrings: TemplateStringsArray,
55
- values: Values,
56
- providedRef?: ElementRef.ElementRef<T>
57
- ) =>
58
- Effect.gen(function*(_) {
59
- const elementRef = providedRef || (yield* _(ElementRef.make<T>()))
60
- const events = Fx.map(elementRef, DomRenderEvent)
61
- const errors = Subject.make<Placeholder.Error<Values[number]>, never>()
62
- const entry = getBrowserEntry(document, ctx, templateStrings)
63
- const content = document.importNode(entry.content, true) // Clone our template
64
-
65
- const parts = yield* _(buildParts(document, ctx, entry.template, content, elementRef, errors.onFailure, false)) // Build runtime-variant of parts with our content.
66
-
67
- // If there are parts we need to render them before constructing our Wire
68
- if (parts.length > 0) {
69
- const refCounter = yield* _(indexRefCounter(parts.length))
70
-
71
- // Do the work
72
- yield* _(renderValues(values, parts, refCounter, errors.onFailure))
73
-
74
- // Wait for initial work to be completed
75
- yield* _(refCounter.wait)
76
- }
528
+ values: Values
529
+ ) => {
530
+ const entry = getBrowserEntry(document, renderContext, templateStrings)
531
+ if (values.length === 0) {
532
+ return Fx.sync(() => DomRenderEvent(persistent(document.importNode(entry.content, true))))
533
+ }
534
+
535
+ return Fx.make<Scope | Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, RenderEvent>((
536
+ sink
537
+ ) => {
538
+ return Effect.gen(function*(_) {
539
+ const content = document.importNode(entry.content, true)
540
+ const context = yield* _(Effect.context<Scope>())
541
+ const refCounter = yield* _(indexRefCounter2())
542
+ const ctx: RenderPartContext = {
543
+ context,
544
+ document,
545
+ eventSource: makeEventSource(),
546
+ expected: 0,
547
+ refCounter,
548
+ renderContext,
549
+ onCause: sink.onFailure as any,
550
+ values
551
+ }
552
+
553
+ // Connect our interpolated values to our template parts
554
+ const effects: Array<Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>> = []
555
+ for (const [part, path] of entry.template.parts) {
556
+ const eff = RenderPartMap[part._tag](part as never, findPath(content, path), ctx)
557
+ if (eff !== null) {
558
+ effects.push(
559
+ ...(Array.isArray(eff) ? eff : [eff]) as Array<
560
+ Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>
561
+ >
562
+ )
563
+ }
564
+ }
565
+
566
+ // Fork any effects necessary
567
+ if (effects.length > 0) {
568
+ yield* _(Effect.forkAll(effects))
569
+ }
570
+
571
+ // If there's anything to wait on and it's not already done, wait for an initial value
572
+ // for all asynchronous sources.
573
+ if (ctx.expected > 0 && (yield* _(refCounter.expect(ctx.expected)))) {
574
+ yield* _(refCounter.wait)
575
+ }
576
+
577
+ // Create a persistent wire from our content
578
+ const wire = persistent(content) as T
77
579
 
78
- // Set the element when it is ready
79
- yield* _(ElementRef.set(elementRef, persistent(content) as T))
580
+ // Set the element when it is ready
581
+ yield* _(ctx.eventSource.setup(wire, Context.get(context, Scope)))
80
582
 
81
- // Return the Template instance
82
- return TemplateInstance(Fx.merge([events, errors]), elementRef)
583
+ // Emity our DomRenderEvent
584
+ yield* _(sink.onSuccess(DomRenderEvent(wire)))
585
+
586
+ // Ensure our templates last forever in the DOM environment
587
+ // so event listeners are kept attached to the current Scope.
588
+ yield* _(Effect.never)
589
+ })
83
590
  })
591
+ }
84
592
 
85
593
  export function renderValues<Values extends ReadonlyArray<Renderable<any, any>>>(
86
594
  values: Values,
87
595
  parts: Parts,
88
596
  refCounter: IndexRefCounter,
89
- onCause: (cause: Cause<Placeholder.Error<Values[number]>>) => Effect.Effect<never, never, unknown>,
597
+ ctx: Context.Context<any> | Context.Context<never>,
90
598
  makeHydrateContext?: (index: number) => HydrateContext
91
599
  ): Effect.Effect<Placeholder.Context<Values[number]> | Scope, never, void> {
92
600
  return Effect.all(parts.map((part, index) => {
@@ -101,11 +609,11 @@ export function renderValues<Values extends ReadonlyArray<Renderable<any, any>>>
101
609
  values,
102
610
  part,
103
611
  refCounter,
104
- onCause,
612
+ ctx,
105
613
  makeHydrateContext ? () => makeHydrateContext(index) : undefined
106
614
  )
107
615
  }
108
- })) as any
616
+ }))
109
617
  }
110
618
 
111
619
  export function renderSparsePart(
@@ -130,63 +638,78 @@ export function renderPart<Values extends ReadonlyArray<Renderable<any, any>>>(
130
638
  values: Values,
131
639
  part: Part,
132
640
  refCounter: IndexRefCounter,
133
- onCause: (cause: Cause<Placeholder.Error<Values[number]>>) => Effect.Effect<never, never, unknown>,
641
+ ctx: Context.Context<any> | Context.Context<never>,
134
642
  hydrateCtx?: () => HydrateContext
135
643
  ): Effect.Effect<any, never, void> {
136
644
  const partIndex = part.index
137
645
  const renderable = values[partIndex]
138
646
 
647
+ if (renderable === null || renderable === undefined) return refCounter.release(partIndex)
648
+
139
649
  if (isDirective(renderable)) {
140
650
  return renderable(part).pipe(
141
- Effect.tap(() => refCounter.release(partIndex)),
651
+ Effect.flatMap(() => refCounter.release(partIndex)),
142
652
  Effect.forkScoped
143
653
  )
144
654
  } else if (part._tag === "ref") {
145
655
  return refCounter.release(partIndex)
146
656
  } else if (part._tag === "event") {
147
- return Effect.tap(
148
- part.update(
149
- getEventHandler(values[partIndex], onCause) as EventHandler.EventHandler<
150
- Placeholder.Context<Values[number]>,
151
- never
152
- >
153
- ),
154
- () => refCounter.release(partIndex)
155
- )
156
- } else if (part._tag === "node" && hydrateCtx) {
157
- if (renderable === null || renderable === undefined) return refCounter.release(partIndex)
657
+ const handler = getEventHandler(renderable, ctx, part.onCause)
658
+ if (handler) {
659
+ part.addEventListener(handler)
660
+ }
158
661
 
662
+ return refCounter.release(partIndex)
663
+ } else if (part._tag === "node" && hydrateCtx) {
159
664
  return handlePart(
160
- values[partIndex],
161
- (value) => Effect.tap(part.update(value), () => refCounter.release(partIndex))
665
+ renderable,
666
+ (value) => Effect.flatMap(part.update(value), () => refCounter.release(partIndex))
162
667
  ).pipe(
163
668
  HydrateContext.provide(hydrateCtx()),
164
669
  Effect.forkScoped
165
670
  )
671
+ } else if (part._tag === "properties") {
672
+ return handlePropertiesPart(renderable, part, refCounter)
166
673
  } else {
167
- const renderable = values[partIndex]
168
-
169
- if (renderable === null || renderable === undefined) return refCounter.release(partIndex)
674
+ return handlePart(
675
+ renderable,
676
+ (value) => Effect.flatMap(part.update(value as any), () => refCounter.release(partIndex))
677
+ )
678
+ }
679
+ }
170
680
 
681
+ function handlePropertiesPart<R, E>(
682
+ renderable: unknown,
683
+ part: PropertiesPart,
684
+ refCounter: IndexRefCounter
685
+ ): Effect.Effect<R | Scope, E, void> {
686
+ if (renderable && typeof renderable === "object") {
171
687
  return handlePart(
172
- values[partIndex],
173
- (value) => Effect.tap(part.update(value as any), () => refCounter.release(partIndex))
688
+ Fx.struct(Object.fromEntries(Object.entries(renderable).map(([k, v]) => [k, unwrapRenderable(v)] as const))),
689
+ (value) => Effect.tap(part.update(value as any), () => refCounter.release(part.index))
174
690
  )
175
691
  }
692
+
693
+ return Effect.succeed(void 0)
176
694
  }
177
695
 
178
696
  function getEventHandler<R, E>(
179
697
  renderable: any,
698
+ ctx: Context.Context<any> | Context.Context<never>,
180
699
  onCause: (cause: Cause<E>) => Effect.Effect<never, never, unknown>
181
- ): EventHandler.EventHandler<R, never> | null {
700
+ ): EventHandler.EventHandler<never, never> | null {
182
701
  if (renderable && typeof renderable === "object") {
183
702
  if (EventHandler.EventHandlerTypeId in renderable) {
184
703
  return EventHandler.make(
185
- (ev) => Effect.catchAllCause((renderable as EventHandler.EventHandler<R, E>).handler(ev), onCause),
704
+ (ev) =>
705
+ Effect.provide(
706
+ Effect.catchAllCause((renderable as EventHandler.EventHandler<R, E>).handler(ev), onCause),
707
+ ctx as any
708
+ ),
186
709
  (renderable as EventHandler.EventHandler<R, E>).options
187
710
  )
188
711
  } else if (Effect.EffectTypeId in renderable) {
189
- return EventHandler.make(() => Effect.catchAllCause(renderable, onCause))
712
+ return EventHandler.make(() => Effect.provide(Effect.catchAllCause(renderable, onCause), ctx))
190
713
  }
191
714
  }
192
715
 
@@ -204,7 +727,7 @@ function handlePart<R, E>(
204
727
  else if (Array.isArray(renderable)) {
205
728
  return renderable.length === 0
206
729
  ? update(null)
207
- : Effect.forkScoped(Fx.observe(Fx.combine(renderable.map(unwrapRenderable)) as any, update))
730
+ : Effect.forkScoped(Fx.observe(Fx.tuple(renderable.map(unwrapRenderable)) as any, update))
208
731
  } else if (TypeId in renderable) {
209
732
  return Effect.forkScoped(Fx.observe(renderable as any, update))
210
733
  } else if (Effect.EffectTypeId in renderable) {
@@ -222,7 +745,7 @@ function unwrapRenderable<R, E>(renderable: unknown): Fx.Fx<R, E, any> {
222
745
  case "object": {
223
746
  if (renderable === null || renderable === undefined) return Fx.succeed(null)
224
747
  else if (Array.isArray(renderable)) {
225
- return renderable.length === 0 ? Fx.succeed(null) : Fx.combine(renderable.map(unwrapRenderable)) as any
748
+ return renderable.length === 0 ? Fx.succeed(null) : Fx.tuple(renderable.map(unwrapRenderable)) as any
226
749
  } else if (TypeId in renderable) {
227
750
  return renderable as any
228
751
  } else if (Effect.EffectTypeId in renderable) {
@@ -238,7 +761,7 @@ function unwrapSparsePartRenderables(
238
761
  renderables: ReadonlyArray<Renderable<any, any>>,
239
762
  part: SparsePart
240
763
  ) {
241
- return Fx.combine(
764
+ return Fx.tuple(
242
765
  // @ts-ignore type too deep
243
766
  renderables.map((renderable, i) => {
244
767
  const p = part.parts[i]
@@ -321,52 +844,56 @@ export function getBrowserEntry(
321
844
  }
322
845
  }
323
846
 
324
- export function buildParts<T extends Rendered, E>(
847
+ export function buildParts<E>(
325
848
  document: Document,
326
849
  ctx: RenderContext,
327
850
  template: Template.Template,
328
851
  content: ParentChildNodes,
329
- ref: ElementRef.ElementRef<T>,
852
+ eventSource: EventSource,
330
853
  onCause: (cause: Cause<E>) => Effect.Effect<never, never, void>,
331
854
  isHydrating: boolean
332
- ): Effect.Effect<Scope, never, Parts> {
333
- return Effect.all(
334
- template.parts.map(([part, path]) =>
335
- buildPartWithNode(document, ctx, part, findPath(content, path), ref, onCause, isHydrating)
336
- )
855
+ ): Parts {
856
+ return template.parts.map(([part, path]) =>
857
+ buildPartWithNode(document, ctx, part, findPath(content, path), eventSource, onCause, isHydrating)
337
858
  )
338
859
  }
339
860
 
340
- function buildPartWithNode<T extends Rendered, E>(
861
+ function buildPartWithNode<E>(
341
862
  document: Document,
342
863
  ctx: RenderContext,
343
864
  part: Template.PartNode | Template.SparsePartNode,
344
865
  node: Node,
345
- ref: ElementRef.ElementRef<T>,
866
+ eventSource: EventSource,
346
867
  onCause: (cause: Cause<E>) => Effect.Effect<never, never, void>,
347
868
  isHydrating: boolean
348
- ): Effect.Effect<Scope, never, Part | SparsePart> {
869
+ ): Part | SparsePart {
349
870
  switch (part._tag) {
350
871
  case "attr":
351
- return Effect.succeed(AttributePartImpl.browser(part.index, node as Element, part.name, ctx))
872
+ return AttributePartImpl.browser(part.index, node as Element, part.name, ctx)
352
873
  case "boolean-part":
353
- return Effect.succeed(BooleanPartImpl.browser(part.index, node as Element, part.name, ctx))
874
+ return BooleanPartImpl.browser(part.index, node as Element, part.name, ctx)
354
875
  case "className-part":
355
- return Effect.succeed(ClassNamePartImpl.browser(part.index, node as Element, ctx))
876
+ return ClassNamePartImpl.browser(part.index, node as Element, ctx)
356
877
  case "comment-part":
357
- return Effect.succeed(CommentPartImpl.browser(part.index, node as Comment, ctx))
878
+ return CommentPartImpl.browser(part.index, node as Comment, ctx)
358
879
  case "data":
359
- return Effect.succeed(DataPartImpl.browser(part.index, node as HTMLElement | SVGElement, ctx))
880
+ return DataPartImpl.browser(part.index, node as HTMLElement | SVGElement, ctx)
360
881
  case "event":
361
- return EventPartImpl.browser(part.name, part.index, ref, node as HTMLElement | SVGElement, onCause) as any
362
- case "node":
363
- return Effect.succeed(
364
- makeRenderNodePart(part.index, node as HTMLElement | SVGElement, ctx, document, isHydrating)
882
+ return new EventPartImpl(
883
+ part.name,
884
+ part.index,
885
+ ElementSource.fromElement(node as Element),
886
+ onCause as any,
887
+ (handler) => eventSource.addEventListener(node as Element, part.name, handler)
365
888
  )
889
+ case "node":
890
+ return makeRenderNodePart(part.index, node as HTMLElement | SVGElement, ctx, document, isHydrating)
366
891
  case "property":
367
- return Effect.succeed(PropertyPartImpl.browser(part.index, node, part.name, ctx))
892
+ return PropertyPartImpl.browser(part.index, node, part.name, ctx)
893
+ case "properties":
894
+ return PropertiesPartImpl.browser(part.index, node as HTMLElement | SVGElement, ctx)
368
895
  case "ref":
369
- return Effect.succeed(new RefPartImpl(ref.query(node as HTMLElement | SVGElement), part.index)) as any
896
+ return new RefPartImpl(ElementSource.fromElement(node as Element), part.index) as any
370
897
  case "sparse-attr": {
371
898
  const parts: Array<AttributePart | StaticText> = Array(part.nodes.length)
372
899
  const sparse = SparseAttributePartImpl.browser(
@@ -394,7 +921,7 @@ function buildPartWithNode<T extends Rendered, E>(
394
921
  }
395
922
  }
396
923
 
397
- return Effect.succeed(sparse)
924
+ return sparse
398
925
  }
399
926
  case "sparse-class-name": {
400
927
  const parts: Array<ClassNamePart | StaticText> = []
@@ -424,7 +951,7 @@ function buildPartWithNode<T extends Rendered, E>(
424
951
  }
425
952
  }
426
953
 
427
- return Effect.succeed(sparse)
954
+ return sparse
428
955
  }
429
956
  case "sparse-comment": {
430
957
  const parts: Array<CommentPart | StaticText> = Array(part.nodes.length)
@@ -451,10 +978,10 @@ function buildPartWithNode<T extends Rendered, E>(
451
978
  }
452
979
  }
453
980
 
454
- return Effect.succeed(sparse)
981
+ return sparse
455
982
  }
456
983
  case "text-part":
457
- return Effect.succeed(TextPartImpl.browser(document, part.index, node as Element, ctx))
984
+ return TextPartImpl.browser(document, part.index, node as Element, ctx)
458
985
  }
459
986
  }
460
987
 
@@ -484,6 +1011,39 @@ function buildNode(document: Document, node: Template.Node, isSvgContext: boolea
484
1011
  case "comment-part":
485
1012
  case "node":
486
1013
  return document.createComment(`hole${node.index}`)
1014
+ case "doctype":
1015
+ return document.implementation.createDocumentType(
1016
+ node.name,
1017
+ docTypeNameToPublicId(node.name),
1018
+ docTypeNameToSystemId(node.name)
1019
+ )
1020
+ }
1021
+ }
1022
+
1023
+ function docTypeNameToPublicId(name: string): string {
1024
+ switch (name) {
1025
+ case "html":
1026
+ return "-//W3C//DTD HTML 4.01//EN"
1027
+ case "svg":
1028
+ return "-//W3C//DTD SVG 1.1//EN"
1029
+ case "math":
1030
+ return "-//W3C//DTD MathML 2.0//EN"
1031
+ default:
1032
+ return ""
1033
+ }
1034
+ }
1035
+
1036
+ function docTypeNameToSystemId(name: string): string {
1037
+ switch (name) {
1038
+ // HTML5
1039
+ case "html":
1040
+ return "http://www.w3.org/TR/html4/strict.dtd"
1041
+ case "svg":
1042
+ return "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
1043
+ case "math":
1044
+ return "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd"
1045
+ default:
1046
+ return ""
487
1047
  }
488
1048
  }
489
1049
 
@@ -527,3 +1087,55 @@ function buildTextChild(document: Document, node: Template.Text): globalThis.Nod
527
1087
 
528
1088
  return document.createComment(`hole${node.index}`)
529
1089
  }
1090
+
1091
+ function createAttribute(
1092
+ document: Document,
1093
+ element: HTMLElement | SVGElement,
1094
+ name: string
1095
+ ): Attr {
1096
+ return element.getAttributeNode(name) ?? document.createAttribute(name)
1097
+ }
1098
+
1099
+ function matchSettablePart(
1100
+ renderable: Renderable<any, any>,
1101
+ setValue: (value: any) => void,
1102
+ makePart: () => Part,
1103
+ schedule: (f: () => void) => Effect.Effect<Scope, never, void>,
1104
+ expect: () => void
1105
+ ) {
1106
+ return matchRenderable(renderable, {
1107
+ Fx: (fx) => {
1108
+ expect()
1109
+ return Fx.observe(fx, (a) => schedule(() => setValue(a)))
1110
+ },
1111
+ Effect: (effect) => {
1112
+ expect()
1113
+ return Effect.flatMap(effect, (a) => schedule(() => setValue(a)))
1114
+ },
1115
+ Directive: (directive) => {
1116
+ expect()
1117
+ return directive(makePart())
1118
+ },
1119
+ Otherwise: (otherwise) => {
1120
+ setValue(otherwise)
1121
+ return null
1122
+ }
1123
+ })
1124
+ }
1125
+
1126
+ function matchRenderable(renderable: Renderable<any, any>, matches: {
1127
+ Fx: (fx: Fx.Fx<any, any, any>) => Effect.Effect<any, any, void> | null
1128
+ Effect: (effect: Effect.Effect<any, any, any>) => Effect.Effect<any, any, void> | null
1129
+ Directive: (directive: Directive<any, any>) => Effect.Effect<any, any, void> | null
1130
+ Otherwise: (_: Renderable<any, any>) => Effect.Effect<any, any, void> | null
1131
+ }): Effect.Effect<any, any, void> | null {
1132
+ if (Fx.isFx(renderable)) {
1133
+ return matches.Fx(renderable)
1134
+ } else if (Effect.isEffect(renderable)) {
1135
+ return matches.Effect(renderable)
1136
+ } else if (isDirective<any, any>(renderable)) {
1137
+ return matches.Directive(renderable)
1138
+ } else {
1139
+ return matches.Otherwise(renderable)
1140
+ }
1141
+ }