@typed/template 0.2.0 → 0.3.1

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 (227) 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/chunks.js +4 -1
  42. package/dist/cjs/internal/chunks.js.map +1 -1
  43. package/dist/cjs/internal/errors.js +4 -0
  44. package/dist/cjs/internal/errors.js.map +1 -1
  45. package/dist/cjs/internal/hydrate.js +113 -80
  46. package/dist/cjs/internal/hydrate.js.map +1 -1
  47. package/dist/cjs/internal/indexRefCounter.js +49 -2
  48. package/dist/cjs/internal/indexRefCounter.js.map +1 -1
  49. package/dist/cjs/internal/parser.js +72 -21
  50. package/dist/cjs/internal/parser.js.map +1 -1
  51. package/dist/cjs/internal/parts.js +128 -28
  52. package/dist/cjs/internal/parts.js.map +1 -1
  53. package/dist/cjs/internal/render.js +460 -161
  54. package/dist/cjs/internal/render.js.map +1 -1
  55. package/dist/cjs/internal/server.js +5 -2
  56. package/dist/cjs/internal/server.js.map +1 -1
  57. package/dist/dts/Directive.d.ts.map +1 -1
  58. package/dist/dts/ElementRef.d.ts +4 -2
  59. package/dist/dts/ElementRef.d.ts.map +1 -1
  60. package/dist/dts/ElementSource.d.ts +10 -5
  61. package/dist/dts/ElementSource.d.ts.map +1 -1
  62. package/dist/dts/EventHandler.d.ts.map +1 -1
  63. package/dist/dts/Html.d.ts +1 -1
  64. package/dist/dts/Html.d.ts.map +1 -1
  65. package/dist/dts/HtmlChunk.d.ts.map +1 -1
  66. package/dist/dts/Many.d.ts +13 -11
  67. package/dist/dts/Many.d.ts.map +1 -1
  68. package/dist/dts/Parser.d.ts +3 -6
  69. package/dist/dts/Parser.d.ts.map +1 -1
  70. package/dist/dts/Part.d.ts +13 -3
  71. package/dist/dts/Part.d.ts.map +1 -1
  72. package/dist/dts/Placeholder.d.ts +2 -2
  73. package/dist/dts/Placeholder.d.ts.map +1 -1
  74. package/dist/dts/Render.d.ts +2 -1
  75. package/dist/dts/Render.d.ts.map +1 -1
  76. package/dist/dts/RenderContext.d.ts +5 -4
  77. package/dist/dts/RenderContext.d.ts.map +1 -1
  78. package/dist/dts/RenderTemplate.d.ts +2 -16
  79. package/dist/dts/RenderTemplate.d.ts.map +1 -1
  80. package/dist/dts/Renderable.d.ts +2 -2
  81. package/dist/dts/Renderable.d.ts.map +1 -1
  82. package/dist/dts/Template.d.ts +21 -3
  83. package/dist/dts/Template.d.ts.map +1 -1
  84. package/dist/dts/TemplateInstance.d.ts +3 -2
  85. package/dist/dts/TemplateInstance.d.ts.map +1 -1
  86. package/dist/dts/Test.d.ts +4 -6
  87. package/dist/dts/Test.d.ts.map +1 -1
  88. package/dist/dts/index.d.ts +0 -4
  89. package/dist/dts/index.d.ts.map +1 -1
  90. package/dist/dts/internal/EventSource.d.ts +12 -0
  91. package/dist/dts/internal/EventSource.d.ts.map +1 -0
  92. package/dist/dts/internal/browser.d.ts.map +1 -1
  93. package/dist/dts/internal/chunks.d.ts +1 -0
  94. package/dist/dts/internal/chunks.d.ts.map +1 -1
  95. package/dist/dts/internal/errors.d.ts +1 -0
  96. package/dist/dts/internal/errors.d.ts.map +1 -1
  97. package/dist/dts/internal/hydrate.d.ts +9 -16
  98. package/dist/dts/internal/hydrate.d.ts.map +1 -1
  99. package/dist/dts/internal/indexRefCounter.d.ts +5 -0
  100. package/dist/dts/internal/indexRefCounter.d.ts.map +1 -1
  101. package/dist/dts/internal/module-augmentation.d.ts +0 -4
  102. package/dist/dts/internal/module-augmentation.d.ts.map +1 -1
  103. package/dist/dts/internal/parser.d.ts +8 -0
  104. package/dist/dts/internal/parser.d.ts.map +1 -1
  105. package/dist/dts/internal/parts.d.ts +66 -56
  106. package/dist/dts/internal/parts.d.ts.map +1 -1
  107. package/dist/dts/internal/render.d.ts +1 -15
  108. package/dist/dts/internal/render.d.ts.map +1 -1
  109. package/dist/dts/internal/server.d.ts.map +1 -1
  110. package/dist/esm/Directive.js +1 -1
  111. package/dist/esm/Directive.js.map +1 -1
  112. package/dist/esm/ElementRef.js +12 -7
  113. package/dist/esm/ElementRef.js.map +1 -1
  114. package/dist/esm/ElementSource.js +16 -13
  115. package/dist/esm/ElementSource.js.map +1 -1
  116. package/dist/esm/EventHandler.js +1 -1
  117. package/dist/esm/EventHandler.js.map +1 -1
  118. package/dist/esm/Html.js +29 -24
  119. package/dist/esm/Html.js.map +1 -1
  120. package/dist/esm/HtmlChunk.js +6 -1
  121. package/dist/esm/HtmlChunk.js.map +1 -1
  122. package/dist/esm/Hydrate.js +1 -1
  123. package/dist/esm/Hydrate.js.map +1 -1
  124. package/dist/esm/Many.js +14 -10
  125. package/dist/esm/Many.js.map +1 -1
  126. package/dist/esm/Parser.js +6 -335
  127. package/dist/esm/Parser.js.map +1 -1
  128. package/dist/esm/Placeholder.js +2 -2
  129. package/dist/esm/Placeholder.js.map +1 -1
  130. package/dist/esm/Platform.js +2 -2
  131. package/dist/esm/Platform.js.map +1 -1
  132. package/dist/esm/Render.js +1 -1
  133. package/dist/esm/Render.js.map +1 -1
  134. package/dist/esm/RenderContext.js +38 -17
  135. package/dist/esm/RenderContext.js.map +1 -1
  136. package/dist/esm/RenderTemplate.js +2 -12
  137. package/dist/esm/RenderTemplate.js.map +1 -1
  138. package/dist/esm/Template.js +24 -0
  139. package/dist/esm/Template.js.map +1 -1
  140. package/dist/esm/TemplateInstance.js +2 -2
  141. package/dist/esm/TemplateInstance.js.map +1 -1
  142. package/dist/esm/Test.js +20 -7
  143. package/dist/esm/Test.js.map +1 -1
  144. package/dist/esm/index.js +0 -4
  145. package/dist/esm/index.js.map +1 -1
  146. package/dist/esm/internal/EventSource.js +91 -0
  147. package/dist/esm/internal/EventSource.js.map +1 -0
  148. package/dist/esm/internal/browser.js +12 -12
  149. package/dist/esm/internal/browser.js.map +1 -1
  150. package/dist/esm/internal/chunks.js +2 -0
  151. package/dist/esm/internal/chunks.js.map +1 -1
  152. package/dist/esm/internal/errors.js +3 -0
  153. package/dist/esm/internal/errors.js.map +1 -1
  154. package/dist/esm/internal/hydrate.js +113 -76
  155. package/dist/esm/internal/hydrate.js.map +1 -1
  156. package/dist/esm/internal/indexRefCounter.js +50 -2
  157. package/dist/esm/internal/indexRefCounter.js.map +1 -1
  158. package/dist/esm/internal/parser.js +98 -22
  159. package/dist/esm/internal/parser.js.map +1 -1
  160. package/dist/esm/internal/parts.js +110 -27
  161. package/dist/esm/internal/parts.js.map +1 -1
  162. package/dist/esm/internal/render.js +446 -157
  163. package/dist/esm/internal/render.js.map +1 -1
  164. package/dist/esm/internal/server.js +5 -4
  165. package/dist/esm/internal/server.js.map +1 -1
  166. package/package.json +10 -26
  167. package/src/Directive.ts +1 -1
  168. package/src/ElementRef.ts +18 -14
  169. package/src/ElementSource.ts +62 -47
  170. package/src/EventHandler.ts +1 -1
  171. package/src/Html.ts +58 -57
  172. package/src/HtmlChunk.ts +15 -1
  173. package/src/Hydrate.ts +1 -1
  174. package/src/Many.ts +53 -43
  175. package/src/Parser.ts +10 -453
  176. package/src/Part.ts +15 -3
  177. package/src/Placeholder.ts +4 -4
  178. package/src/Platform.ts +2 -2
  179. package/src/Render.ts +7 -2
  180. package/src/RenderContext.ts +47 -19
  181. package/src/RenderTemplate.ts +9 -54
  182. package/src/Renderable.ts +2 -1
  183. package/src/Template.ts +26 -0
  184. package/src/TemplateInstance.ts +9 -9
  185. package/src/Test.ts +40 -21
  186. package/src/index.ts +0 -4
  187. package/src/internal/EventSource.ts +153 -0
  188. package/src/internal/browser.ts +26 -25
  189. package/src/internal/chunks.ts +4 -0
  190. package/src/internal/errors.ts +4 -0
  191. package/src/internal/hydrate.ts +161 -107
  192. package/src/internal/indexRefCounter.ts +63 -2
  193. package/src/internal/module-augmentation.ts +0 -4
  194. package/src/internal/parser.ts +107 -25
  195. package/src/internal/parts.ts +158 -73
  196. package/src/internal/render.ts +638 -289
  197. package/src/internal/server.ts +5 -3
  198. package/Token/package.json +0 -6
  199. package/Tokenizer/package.json +0 -6
  200. package/dist/cjs/Token.js +0 -270
  201. package/dist/cjs/Token.js.map +0 -1
  202. package/dist/cjs/Tokenizer.js +0 -18
  203. package/dist/cjs/Tokenizer.js.map +0 -1
  204. package/dist/cjs/internal/readAttribute.js +0 -34
  205. package/dist/cjs/internal/readAttribute.js.map +0 -1
  206. package/dist/cjs/internal/tokenizer.js +0 -264
  207. package/dist/cjs/internal/tokenizer.js.map +0 -1
  208. package/dist/dts/Token.d.ts +0 -202
  209. package/dist/dts/Token.d.ts.map +0 -1
  210. package/dist/dts/Tokenizer.d.ts +0 -6
  211. package/dist/dts/Tokenizer.d.ts.map +0 -1
  212. package/dist/dts/internal/readAttribute.d.ts +0 -9
  213. package/dist/dts/internal/readAttribute.d.ts.map +0 -1
  214. package/dist/dts/internal/tokenizer.d.ts +0 -3
  215. package/dist/dts/internal/tokenizer.d.ts.map +0 -1
  216. package/dist/esm/Token.js +0 -264
  217. package/dist/esm/Token.js.map +0 -1
  218. package/dist/esm/Tokenizer.js +0 -9
  219. package/dist/esm/Tokenizer.js.map +0 -1
  220. package/dist/esm/internal/readAttribute.js +0 -24
  221. package/dist/esm/internal/readAttribute.js.map +0 -1
  222. package/dist/esm/internal/tokenizer.js +0 -296
  223. package/dist/esm/internal/tokenizer.js.map +0 -1
  224. package/src/Token.ts +0 -269
  225. package/src/Tokenizer.ts +0 -10
  226. package/src/internal/readAttribute.ts +0 -28
  227. package/src/internal/tokenizer.ts +0 -338
@@ -1,17 +1,19 @@
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"
8
- import { replace } from "effect/ReadonlyArray"
9
- import type { Scope } from "effect/Scope"
7
+ import type { Chunk } from "effect/Chunk"
8
+ import * as Context from "effect/Context"
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 { Part } from "../Part.js"
15
17
  import type { Placeholder } from "../Placeholder.js"
16
18
  import type { ToRendered } from "../Render.js"
17
19
  import type { Renderable } from "../Renderable.js"
@@ -20,11 +22,11 @@ import type { RenderEvent } from "../RenderEvent.js"
20
22
  import { DomRenderEvent } from "../RenderEvent.js"
21
23
  import type { RenderTemplate } from "../RenderTemplate.js"
22
24
  import type * as Template from "../Template.js"
23
- import { TemplateInstance } from "../TemplateInstance.js"
24
25
  import { makeRenderNodePart } from "./browser.js"
26
+ import { type EventSource, makeEventSource } from "./EventSource.js"
25
27
  import { HydrateContext } from "./HydrateContext.js"
26
- import type { IndexRefCounter } from "./indexRefCounter.js"
27
- import { indexRefCounter } from "./indexRefCounter.js"
28
+ import type { IndexRefCounter2 } from "./indexRefCounter.js"
29
+ import { indexRefCounter2 } from "./indexRefCounter.js"
28
30
  import { parse } from "./parser.js"
29
31
  import {
30
32
  AttributePartImpl,
@@ -32,161 +34,585 @@ import {
32
34
  ClassNamePartImpl,
33
35
  CommentPartImpl,
34
36
  DataPartImpl,
35
- EventPartImpl,
36
37
  PropertyPartImpl,
37
38
  RefPartImpl,
38
- SparseAttributePartImpl,
39
- SparseClassNamePartImpl,
40
- SparseCommentPartImpl,
41
- StaticTextImpl,
42
39
  TextPartImpl
43
40
  } from "./parts.js"
44
41
  import type { ParentChildNodes } from "./utils.js"
45
- import { findPath } from "./utils.js"
42
+ import { findHoleComment, findPath } from "./utils.js"
43
+
44
+ // TODO: We need to add support for hydration of templates
45
+ // TODO: We need to re-think hydration for dynamic lists, probably just markers should be fine
46
+ // TODO: We need to make Parts synchronous
46
47
 
47
48
  /**
48
- * Here for "standard" browser rendering, a TemplateInstance is effectively a live
49
- * view into the contents rendered by the Template.
49
+ * @internal
50
50
  */
51
- export const renderTemplate: (document: Document, ctx: RenderContext) => RenderTemplate =
52
- (document, ctx) =>
53
- <Values extends ReadonlyArray<Renderable<any, any>>, T extends Rendered = Rendered>(
54
- 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)
51
+ export type RenderPartContext = {
52
+ readonly context: Context.Context<Scope>
53
+ readonly document: Document
54
+ readonly eventSource: EventSource
55
+ readonly refCounter: IndexRefCounter2
56
+ readonly renderContext: RenderContext
57
+ readonly values: ReadonlyArray<Renderable<any, any>>
58
+ readonly onCause: (cause: Cause<any>) => Effect.Effect<never, never, void>
59
+
60
+ readonly makeHydrateContext?: (index: number) => HydrateContext
61
+
62
+ expected: number
63
+ }
64
+
65
+ type RenderPartMap = {
66
+ readonly [K in Template.PartNode["_tag"] | Template.SparsePartNode["_tag"]]: (
67
+ part: Extract<Template.PartNode | Template.SparsePartNode, { _tag: K }>,
68
+ node: Node,
69
+ ctx: RenderPartContext
70
+ ) => null | Effect.Effect<any, any, void> | Array<Effect.Effect<any, any, void>>
71
+ }
72
+
73
+ const RenderPartMap: RenderPartMap = {
74
+ "attr": (templatePart, node, ctx) => {
75
+ const { document, refCounter, renderContext, values } = ctx
76
+ const element = node as HTMLElement | SVGElement
77
+ const attr = createAttribute(document, element, templatePart.name)
78
+ const renderable = values[templatePart.index]
79
+ let isSet = true
80
+ const setValue = (value: string | null | undefined) => {
81
+ if (isNullOrUndefined(value)) {
82
+ element.removeAttribute(templatePart.name)
83
+ isSet = false
84
+ } else {
85
+ attr.value = String(value)
86
+ if (isSet === false) {
87
+ element.setAttributeNode(attr)
88
+ isSet = true
89
+ }
76
90
  }
91
+ }
77
92
 
78
- // Set the element when it is ready
79
- yield* _(ElementRef.set(elementRef, persistent(content) as T))
93
+ return matchSettablePart(
94
+ renderable,
95
+ setValue,
96
+ () => AttributePartImpl.browser(templatePart.index, element, templatePart.name, renderContext),
97
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
98
+ () => ctx.expected++
99
+ )
100
+ },
101
+ "boolean-part": (templatePart, node, ctx) => {
102
+ const { refCounter, renderContext, values } = ctx
103
+ const element = node as HTMLElement | SVGElement
104
+ const renderable = values[templatePart.index]
105
+ const setValue = (value: boolean | null | undefined) => {
106
+ element.toggleAttribute(templatePart.name, isNullOrUndefined(value) ? false : Boolean(value))
107
+ }
80
108
 
81
- // Return the Template instance
82
- return TemplateInstance(Fx.merge([events, errors]), elementRef)
83
- })
109
+ return matchSettablePart(
110
+ renderable,
111
+ setValue,
112
+ () => BooleanPartImpl.browser(templatePart.index, element, templatePart.name, renderContext),
113
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
114
+ () => ctx.expected++
115
+ )
116
+ },
117
+ "className-part": (templatePart, node, ctx) => {
118
+ const { refCounter, renderContext, values } = ctx
119
+ const element = node as HTMLElement | SVGElement
120
+ const renderable = values[templatePart.index]
121
+ let classNames: Set<string> = new Set()
122
+ const setValue = (value: string | Array<string> | null | undefined) => {
123
+ if (isNullOrUndefined(value)) {
124
+ element.classList.remove(...classNames)
125
+ classNames.clear()
126
+ } else {
127
+ const newClassNames = new Set(Array.isArray(value) ? value : [String(value)])
128
+ const { added, removed } = diffClassNames(classNames, newClassNames)
129
+
130
+ if (removed.length > 0) {
131
+ element.classList.remove(...removed)
132
+ }
133
+ if (added.length > 0) element.classList.add(...added)
84
134
 
85
- export function renderValues<Values extends ReadonlyArray<Renderable<any, any>>>(
86
- values: Values,
87
- parts: Parts,
88
- refCounter: IndexRefCounter,
89
- onCause: (cause: Cause<Placeholder.Error<Values[number]>>) => Effect.Effect<never, never, unknown>,
90
- makeHydrateContext?: (index: number) => HydrateContext
91
- ): Effect.Effect<Placeholder.Context<Values[number]> | Scope, never, void> {
92
- return Effect.all(parts.map((part, index) => {
93
- switch (part._tag) {
94
- case "sparse/attribute":
95
- case "sparse/className":
96
- case "sparse/comment": {
97
- return renderSparsePart(values, part, refCounter)
135
+ classNames = newClassNames
98
136
  }
99
- default:
100
- return renderPart(
101
- values,
102
- part,
103
- refCounter,
104
- onCause,
105
- makeHydrateContext ? () => makeHydrateContext(index) : undefined
106
- )
107
137
  }
108
- })) as any
109
- }
110
138
 
111
- export function renderSparsePart(
112
- values: ReadonlyArray<Renderable<any, any>>,
113
- part: SparsePart,
114
- refCounter: IndexRefCounter
115
- ) {
116
- const indexes = part.parts.flatMap((p) => p._tag === "static/text" ? [] : [p.index])
117
-
118
- return Effect.forkScoped(
119
- Fx.observe(
120
- unwrapSparsePartRenderables(
121
- part.parts.map((p) => p._tag === "static/text" ? Fx.succeed(p.value) : values[p.index]),
122
- part
123
- ),
124
- (value) => Effect.tap(part.update(value as any), () => Effect.forEach(indexes, (a) => refCounter.release(a)))
139
+ return matchSettablePart(
140
+ renderable,
141
+ setValue,
142
+ () => ClassNamePartImpl.browser(templatePart.index, element, renderContext),
143
+ (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
144
+ () => ctx.expected++
125
145
  )
126
- )
127
- }
146
+ },
147
+ "comment-part": (templatePart, node, ctx) => {
148
+ const { refCounter, renderContext, values } = ctx
149
+ const comment = findHoleComment(node as Element, templatePart.index)
150
+ const renderable = values[templatePart.index]
151
+ const setValue = (value: string | null | undefined) => {
152
+ comment.textContent = isNullOrUndefined(value) ? "" : String(value)
153
+ }
128
154
 
129
- export function renderPart<Values extends ReadonlyArray<Renderable<any, any>>>(
130
- values: Values,
131
- part: Part,
132
- refCounter: IndexRefCounter,
133
- onCause: (cause: Cause<Placeholder.Error<Values[number]>>) => Effect.Effect<never, never, unknown>,
134
- hydrateCtx?: () => HydrateContext
135
- ): Effect.Effect<any, never, void> {
136
- const partIndex = part.index
137
- const renderable = values[partIndex]
138
-
139
- if (isDirective(renderable)) {
140
- return renderable(part).pipe(
141
- Effect.tap(() => refCounter.release(partIndex)),
142
- Effect.forkScoped
155
+ return matchSettablePart(
156
+ renderable,
157
+ setValue,
158
+ () => CommentPartImpl.browser(templatePart.index, comment, renderContext),
159
+ (f) => Effect.zipRight(renderContext.queue.add(comment, f), refCounter.release(templatePart.index)),
160
+ () => ctx.expected++
143
161
  )
144
- } else if (part._tag === "ref") {
145
- return refCounter.release(partIndex)
146
- } 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)
162
+ },
163
+ "data": (templatePart, node, ctx) => {
164
+ const element = node as HTMLElement | SVGElement
165
+ const renderable = ctx.values[templatePart.index]
166
+ const previousKeys = new Set<string>(Object.keys(element.dataset))
167
+ const setValue = (value: Record<string, string | undefined> | null | undefined) => {
168
+ if (isNullOrUndefined(value)) {
169
+ for (const key of previousKeys) {
170
+ delete element.dataset[key]
171
+ }
172
+ previousKeys.clear()
173
+ } else {
174
+ for (const key of previousKeys) {
175
+ if (!(key in value)) {
176
+ delete element.dataset[key]
177
+ previousKeys.delete(key)
178
+ }
179
+ }
180
+
181
+ for (const key of Object.keys(value)) {
182
+ if (!previousKeys.has(key)) {
183
+ previousKeys.add(key)
184
+ }
185
+ element.dataset[key] = value[key] || ""
186
+ }
187
+ }
188
+ }
189
+
190
+ return matchSettablePart(
191
+ renderable,
192
+ setValue,
193
+ () => DataPartImpl.browser(templatePart.index, element, ctx.renderContext),
194
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(templatePart.index)),
195
+ () => ctx.expected++
155
196
  )
156
- } else if (part._tag === "node" && hydrateCtx) {
157
- if (renderable === null || renderable === undefined) return refCounter.release(partIndex)
197
+ },
198
+ "event": (templatePart, node, ctx) => {
199
+ const element = node as HTMLElement | SVGElement
200
+ const renderable = ctx.values[templatePart.index]
201
+ const handler = getEventHandler(renderable, ctx.context, ctx.onCause)
202
+ if (handler) {
203
+ ctx.eventSource.addEventListener(element, templatePart.name, handler)
204
+ }
158
205
 
159
- return handlePart(
160
- values[partIndex],
161
- (value) => Effect.tap(part.update(value), () => refCounter.release(partIndex))
162
- ).pipe(
163
- HydrateContext.provide(hydrateCtx()),
164
- Effect.forkScoped
206
+ return null
207
+ },
208
+ "node": (templatePart, node, ctx) => {
209
+ const makeHydrateContext = ctx.makeHydrateContext
210
+ const part = makeRenderNodePart(
211
+ templatePart.index,
212
+ node as HTMLElement | SVGElement,
213
+ ctx.renderContext,
214
+ ctx.document,
215
+ !!makeHydrateContext
165
216
  )
166
- } else {
167
- const renderable = values[partIndex]
168
217
 
169
- if (renderable === null || renderable === undefined) return refCounter.release(partIndex)
218
+ ctx.expected++
219
+
220
+ const handle = handlePart(
221
+ ctx.values[templatePart.index],
222
+ (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
223
+ )
224
+
225
+ if (makeHydrateContext) {
226
+ return Effect.provideService(handle, HydrateContext, makeHydrateContext(templatePart.index))
227
+ } else {
228
+ return handle
229
+ }
230
+ },
231
+ "property": (templatePart, node, ctx) => {
232
+ const element = node as HTMLElement | SVGElement
233
+ const renderable = ctx.values[templatePart.index]
234
+ const setValue = (value: string | null | undefined) => {
235
+ if (isNullOrUndefined(value)) {
236
+ delete (element as any)[templatePart.name]
237
+ } else {
238
+ ;(element as any)[templatePart.name] = value
239
+ }
240
+ }
241
+
242
+ return matchSettablePart(
243
+ renderable,
244
+ setValue,
245
+ () => PropertyPartImpl.browser(templatePart.index, element, templatePart.name, ctx.renderContext),
246
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(templatePart.index)),
247
+ () => ctx.expected++
248
+ )
249
+ },
250
+ "properties": (templatePart, node, ctx) => {
251
+ const renderable = ctx.values[templatePart.index] as any as Record<string, any>
252
+
253
+ if (isNullOrUndefined(renderable)) return null
254
+ else if (Fx.isFx(renderable) || Effect.isEffect(renderable)) {
255
+ throw new Error(`Properties Part must utilize an Record of renderable values.`)
256
+ } else if (typeof renderable === "object" && !Array.isArray(renderable)) {
257
+ const element = node as HTMLElement | SVGElement
258
+
259
+ const toggleBoolean = (key: string, value: unknown) => {
260
+ element.toggleAttribute(key, isNullOrUndefined(value) ? false : Boolean(value))
261
+ }
262
+ const setAttribute = (key: string, value: unknown) => {
263
+ if (isNullOrUndefined(value)) {
264
+ element.removeAttribute(key)
265
+ } else {
266
+ element.setAttribute(key, String(value))
267
+ }
268
+ }
269
+ const setProperty = (key: string, value: unknown) => {
270
+ if (isNullOrUndefined(value)) {
271
+ delete (element as any)[key]
272
+ } else {
273
+ ;(element as any)[key] = value
274
+ }
275
+ }
276
+
277
+ const effects: Array<Effect.Effect<any, any, void>> = []
278
+
279
+ // We need indexes to track async values that won't conflict
280
+ // with any other Parts, we can start end of the current values.length
281
+ // As there should only ever be exactly 1 properties part.
282
+ let i = ctx.values.length
283
+
284
+ loop:
285
+ for (const [key, value] of Object.entries(renderable)) {
286
+ const index = ++i
287
+
288
+ switch (key[0]) {
289
+ case "?": {
290
+ const name = key.slice(1)
291
+ const eff = matchSettablePart(
292
+ value,
293
+ (value) => toggleBoolean(name, value),
294
+ () => BooleanPartImpl.browser(index, element, name, ctx.renderContext),
295
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
296
+ () => ctx.expected++
297
+ )
298
+ if (eff !== null) {
299
+ effects.push(eff)
300
+ }
301
+ continue loop
302
+ }
303
+ case ".": {
304
+ const name = key.slice(1)
305
+ const eff = matchSettablePart(
306
+ value,
307
+ (value) => setProperty(name, value),
308
+ () => PropertyPartImpl.browser(index, element, name, ctx.renderContext),
309
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
310
+ () => ctx.expected++
311
+ )
312
+ if (eff !== null) {
313
+ effects.push(eff)
314
+ }
315
+ continue loop
316
+ }
317
+ case "@": {
318
+ const name = key.slice(1)
319
+ const handler = getEventHandler(value, ctx.context, ctx.onCause)
320
+ if (handler) {
321
+ ctx.eventSource.addEventListener(element, name, handler)
322
+ }
323
+ continue loop
324
+ }
325
+ case "o": {
326
+ if (key[1] === "n") {
327
+ const name = key.slice(2)
328
+ const handler = getEventHandler(value, ctx.context, ctx.onCause)
329
+ if (handler) {
330
+ ctx.eventSource.addEventListener(element, name, handler)
331
+ }
332
+ }
333
+ continue loop
334
+ }
335
+ }
336
+
337
+ const eff = matchSettablePart(
338
+ value,
339
+ (value) => setAttribute(key, value),
340
+ () => AttributePartImpl.browser(index, element, key, ctx.renderContext),
341
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
342
+ () => ctx.expected++
343
+ )
344
+ if (eff !== null) {
345
+ effects.push(eff)
346
+ }
347
+ }
348
+
349
+ return effects
350
+ } else {
351
+ return null
352
+ }
353
+ },
354
+ "ref": (templatePart, node, ctx) => {
355
+ const element = node as HTMLElement | SVGElement
356
+ const renderable = ctx.values[templatePart.index]
357
+
358
+ if (isDirective(renderable)) {
359
+ return renderable(new RefPartImpl(ElementSource.fromElement(element), templatePart.index))
360
+ } else if (ElementRef.isElementRef(renderable)) {
361
+ return ElementRef.set(renderable, element)
362
+ }
363
+
364
+ return null
365
+ },
366
+ "sparse-attr": (templatePart, node, ctx) => {
367
+ const values = Array.from({ length: templatePart.nodes.length }, (): string => "")
368
+ const element = node as HTMLElement | SVGElement
369
+ const attr = createAttribute(ctx.document, element, templatePart.name)
370
+
371
+ const setValue = (value: string | null | undefined, index: number) =>
372
+ Effect.suspend(() => {
373
+ values[index] = value || ""
374
+ return ctx.renderContext.queue.add(element, () => attr.value = values.join(""))
375
+ })
376
+
377
+ const effects: Array<Effect.Effect<any, any, void>> = []
378
+
379
+ for (let i = 0; i < templatePart.nodes.length; ++i) {
380
+ const node = templatePart.nodes[i]
381
+ if (node._tag === "text") {
382
+ values[i] = node.value
383
+ } else {
384
+ const renderable = ctx.values[node.index]
385
+ const index = i
386
+ const effect = matchSettablePart(
387
+ renderable,
388
+ (value) => setValue(value, index),
389
+ () =>
390
+ new AttributePartImpl(
391
+ templatePart.name,
392
+ node.index,
393
+ ({ value }) => setValue(value, index),
394
+ null
395
+ ),
396
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(node.index)),
397
+ () => ctx.expected++
398
+ )
399
+
400
+ if (effect !== null) {
401
+ effects.push(effect)
402
+ }
403
+ }
404
+ }
405
+
406
+ return effects
407
+ },
408
+ "sparse-class-name": (templatePart, node, ctx) => {
409
+ const element = node as HTMLElement | SVGElement
410
+
411
+ const effects = templatePart.nodes.flatMap((node) => {
412
+ if (node._tag === "text") {
413
+ const split = splitClassNames(node.value)
414
+ if (split.length > 0) element.classList.add(...split)
415
+ return []
416
+ } else {
417
+ const eff = RenderPartMap[node._tag](node, element, ctx)
418
+ if (eff === null) return []
419
+ return Array.isArray(eff) ? eff : [eff]
420
+ }
421
+ })
422
+
423
+ return effects
424
+ },
425
+ "sparse-comment": (templatePart, node, ctx) => {
426
+ const values = Array.from({ length: templatePart.nodes.length }, (): string => "")
427
+ const comment = node as Comment
428
+
429
+ const setValue = (value: string | null | undefined, index: number) =>
430
+ Effect.suspend(() => {
431
+ values[index] = value || ""
432
+ return ctx.renderContext.queue.add(comment, () => comment.textContent = values.join(""))
433
+ })
434
+
435
+ const effects: Array<Effect.Effect<any, any, void>> = []
436
+
437
+ for (let i = 0; i < templatePart.nodes.length; ++i) {
438
+ const node = templatePart.nodes[i]
439
+ if (node._tag === "text") {
440
+ values[i] = node.value
441
+ } else {
442
+ const renderable = ctx.values[node.index]
443
+ const index = i
444
+ const effect = matchSettablePart(
445
+ renderable,
446
+ (value) => setValue(value, index),
447
+ () =>
448
+ new CommentPartImpl(
449
+ node.index,
450
+ ({ value }) => setValue(value, index),
451
+ null
452
+ ),
453
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(comment, f), ctx.refCounter.release(node.index)),
454
+ () => ctx.expected++
455
+ )
456
+
457
+ if (effect !== null) {
458
+ effects.push(effect)
459
+ }
460
+ }
461
+ }
462
+
463
+ return effects
464
+ },
465
+ "text-part": (templatePart, node, ctx) => {
466
+ const part = TextPartImpl.browser(
467
+ ctx.document,
468
+ templatePart.index,
469
+ node as HTMLElement | SVGElement,
470
+ ctx.renderContext
471
+ )
472
+
473
+ ctx.expected++
170
474
 
171
475
  return handlePart(
172
- values[partIndex],
173
- (value) => Effect.tap(part.update(value as any), () => refCounter.release(partIndex))
476
+ ctx.values[templatePart.index],
477
+ (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
174
478
  )
175
479
  }
176
480
  }
177
481
 
482
+ const SPACE_REGEXP = /\s+/g
483
+
484
+ function splitClassNames(value: string) {
485
+ return value.split(SPACE_REGEXP).flatMap((a) => {
486
+ const trimmed = a.trim()
487
+ return trimmed.length > 0 ? [trimmed] : []
488
+ })
489
+ }
490
+
491
+ function isNullOrUndefined<T>(value: T | null | undefined): value is null | undefined {
492
+ return value === null || value === undefined
493
+ }
494
+
495
+ function diffClassNames(oldClassNames: Set<string>, newClassNames: Set<string>) {
496
+ const added: Array<string> = []
497
+ const removed: Array<string> = []
498
+
499
+ for (const className of oldClassNames) {
500
+ if (!newClassNames.has(className)) {
501
+ removed.push(className)
502
+ }
503
+ }
504
+
505
+ for (const className of newClassNames) {
506
+ if (!oldClassNames.has(className) && className.trim()) {
507
+ added.push(className)
508
+ }
509
+ }
510
+
511
+ return { added, removed }
512
+ }
513
+
514
+ /**
515
+ * @internal
516
+ */
517
+ export function renderPart2(
518
+ part: Template.PartNode | Template.SparsePartNode,
519
+ content: ParentChildNodes,
520
+ path: Chunk<number>,
521
+ ctx: RenderPartContext
522
+ ): Effect.Effect<any, any, void> | Array<Effect.Effect<any, any, void>> | null {
523
+ return RenderPartMap[part._tag](part as any, findPath(content, path), ctx)
524
+ }
525
+
526
+ /**
527
+ * Here for "standard" browser rendering, a TemplateInstance is effectively a live
528
+ * view into the contents rendered by the Template.
529
+ */
530
+ export const renderTemplate: (document: Document, renderContext: RenderContext) => RenderTemplate =
531
+ (document, renderContext) =>
532
+ <Values extends ReadonlyArray<Renderable<any, any>>, T extends Rendered = Rendered>(
533
+ templateStrings: TemplateStringsArray,
534
+ values: Values
535
+ ) => {
536
+ const entry = getBrowserEntry(document, renderContext, templateStrings)
537
+ if (values.length === 0) {
538
+ return Fx.sync(() => DomRenderEvent(persistent(document.importNode(entry.content, true))))
539
+ }
540
+
541
+ return Fx.make<Scope | Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, RenderEvent>((
542
+ sink
543
+ ) => {
544
+ return Effect.gen(function*(_) {
545
+ const content = document.importNode(entry.content, true)
546
+ const context = yield* _(Effect.context<Scope>())
547
+ const refCounter = yield* _(indexRefCounter2())
548
+ const ctx: RenderPartContext = {
549
+ context,
550
+ document,
551
+ eventSource: makeEventSource(),
552
+ expected: 0,
553
+ refCounter,
554
+ renderContext,
555
+ onCause: sink.onFailure as any,
556
+ values
557
+ }
558
+
559
+ // Connect our interpolated values to our template parts
560
+ const effects: Array<Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>> = []
561
+ for (const [part, path] of entry.template.parts) {
562
+ const eff = renderPart2(part, content, path, ctx)
563
+ if (eff !== null) {
564
+ effects.push(
565
+ ...(Array.isArray(eff) ? eff : [eff]) as Array<
566
+ Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>
567
+ >
568
+ )
569
+ }
570
+ }
571
+
572
+ // Fork any effects necessary
573
+ if (effects.length > 0) {
574
+ yield* _(Effect.forkAll(effects))
575
+ }
576
+
577
+ // If there's anything to wait on and it's not already done, wait for an initial value
578
+ // for all asynchronous sources.
579
+ if (ctx.expected > 0 && (yield* _(refCounter.expect(ctx.expected)))) {
580
+ yield* _(refCounter.wait)
581
+ }
582
+
583
+ // Create a persistent wire from our content
584
+ const wire = persistent(content) as T
585
+
586
+ // Set the element when it is ready
587
+ yield* _(ctx.eventSource.setup(wire, Context.get(context, Scope)))
588
+
589
+ // Emit our DomRenderEvent
590
+ yield* _(sink.onSuccess(DomRenderEvent(wire)))
591
+
592
+ // Ensure our templates last forever in the DOM environment
593
+ // so event listeners are kept attached to the current Scope.
594
+ yield* _(Effect.never)
595
+ })
596
+ })
597
+ }
598
+
178
599
  function getEventHandler<R, E>(
179
600
  renderable: any,
601
+ ctx: Context.Context<any> | Context.Context<never>,
180
602
  onCause: (cause: Cause<E>) => Effect.Effect<never, never, unknown>
181
- ): EventHandler.EventHandler<R, never> | null {
603
+ ): EventHandler.EventHandler<never, never> | null {
182
604
  if (renderable && typeof renderable === "object") {
183
605
  if (EventHandler.EventHandlerTypeId in renderable) {
184
606
  return EventHandler.make(
185
- (ev) => Effect.catchAllCause((renderable as EventHandler.EventHandler<R, E>).handler(ev), onCause),
607
+ (ev) =>
608
+ Effect.provide(
609
+ Effect.catchAllCause((renderable as EventHandler.EventHandler<R, E>).handler(ev), onCause),
610
+ ctx as any
611
+ ),
186
612
  (renderable as EventHandler.EventHandler<R, E>).options
187
613
  )
188
614
  } else if (Effect.EffectTypeId in renderable) {
189
- return EventHandler.make(() => Effect.catchAllCause(renderable, onCause))
615
+ return EventHandler.make(() => Effect.provide(Effect.catchAllCause(renderable, onCause), ctx))
190
616
  }
191
617
  }
192
618
 
@@ -204,7 +630,7 @@ function handlePart<R, E>(
204
630
  else if (Array.isArray(renderable)) {
205
631
  return renderable.length === 0
206
632
  ? update(null)
207
- : Effect.forkScoped(Fx.observe(Fx.combine(renderable.map(unwrapRenderable)) as any, update))
633
+ : Effect.forkScoped(Fx.observe(Fx.tuple(renderable.map(unwrapRenderable)) as any, update))
208
634
  } else if (TypeId in renderable) {
209
635
  return Effect.forkScoped(Fx.observe(renderable as any, update))
210
636
  } else if (Effect.EffectTypeId in renderable) {
@@ -222,7 +648,7 @@ function unwrapRenderable<R, E>(renderable: unknown): Fx.Fx<R, E, any> {
222
648
  case "object": {
223
649
  if (renderable === null || renderable === undefined) return Fx.succeed(null)
224
650
  else if (Array.isArray(renderable)) {
225
- return renderable.length === 0 ? Fx.succeed(null) : Fx.combine(renderable.map(unwrapRenderable)) as any
651
+ return renderable.length === 0 ? Fx.succeed(null) : Fx.tuple(renderable.map(unwrapRenderable)) as any
226
652
  } else if (TypeId in renderable) {
227
653
  return renderable as any
228
654
  } else if (Effect.EffectTypeId in renderable) {
@@ -234,31 +660,6 @@ function unwrapRenderable<R, E>(renderable: unknown): Fx.Fx<R, E, any> {
234
660
  }
235
661
  }
236
662
 
237
- function unwrapSparsePartRenderables(
238
- renderables: ReadonlyArray<Renderable<any, any>>,
239
- part: SparsePart
240
- ) {
241
- return Fx.combine(
242
- // @ts-ignore type too deep
243
- renderables.map((renderable, i) => {
244
- const p = part.parts[i]
245
-
246
- if (p._tag === "static/text") {
247
- return Fx.succeed(p.value)
248
- }
249
-
250
- if (isDirective(renderable)) {
251
- return Fx.fromEffect(Effect.map(renderable(p), () => p.value))
252
- }
253
-
254
- return Fx.mapEffect(
255
- unwrapRenderable(renderable),
256
- (u) => Effect.map(p.update(u), () => p.value)
257
- )
258
- })
259
- ) as any
260
- }
261
-
262
663
  export function attachRoot<T extends RenderEvent | null>(
263
664
  cache: RenderContext["renderCache"],
264
665
  where: HTMLElement,
@@ -321,143 +722,6 @@ export function getBrowserEntry(
321
722
  }
322
723
  }
323
724
 
324
- export function buildParts<T extends Rendered, E>(
325
- document: Document,
326
- ctx: RenderContext,
327
- template: Template.Template,
328
- content: ParentChildNodes,
329
- ref: ElementRef.ElementRef<T>,
330
- onCause: (cause: Cause<E>) => Effect.Effect<never, never, void>,
331
- 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
- )
337
- )
338
- }
339
-
340
- function buildPartWithNode<T extends Rendered, E>(
341
- document: Document,
342
- ctx: RenderContext,
343
- part: Template.PartNode | Template.SparsePartNode,
344
- node: Node,
345
- ref: ElementRef.ElementRef<T>,
346
- onCause: (cause: Cause<E>) => Effect.Effect<never, never, void>,
347
- isHydrating: boolean
348
- ): Effect.Effect<Scope, never, Part | SparsePart> {
349
- switch (part._tag) {
350
- case "attr":
351
- return Effect.succeed(AttributePartImpl.browser(part.index, node as Element, part.name, ctx))
352
- case "boolean-part":
353
- return Effect.succeed(BooleanPartImpl.browser(part.index, node as Element, part.name, ctx))
354
- case "className-part":
355
- return Effect.succeed(ClassNamePartImpl.browser(part.index, node as Element, ctx))
356
- case "comment-part":
357
- return Effect.succeed(CommentPartImpl.browser(part.index, node as Comment, ctx))
358
- case "data":
359
- return Effect.succeed(DataPartImpl.browser(part.index, node as HTMLElement | SVGElement, ctx))
360
- 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)
365
- )
366
- case "property":
367
- return Effect.succeed(PropertyPartImpl.browser(part.index, node, part.name, ctx))
368
- case "ref":
369
- return Effect.succeed(new RefPartImpl(ref.query(node as HTMLElement | SVGElement), part.index)) as any
370
- case "sparse-attr": {
371
- const parts: Array<AttributePart | StaticText> = Array(part.nodes.length)
372
- const sparse = SparseAttributePartImpl.browser(
373
- part.name,
374
- parts,
375
- node as HTMLElement | SVGElement,
376
- ctx
377
- )
378
-
379
- for (let i = 0; i < part.nodes.length; ++i) {
380
- const node = part.nodes[i]
381
-
382
- if (node._tag === "text") {
383
- parts.push(new StaticTextImpl(node.value))
384
- ;(sparse as any).value[i] = node.value
385
- } else {
386
- parts.push(
387
- new AttributePartImpl(
388
- node.name,
389
- node.index,
390
- ({ value }) => sparse.update(replace(sparse.value, i, value || "")),
391
- sparse.value[i]
392
- )
393
- )
394
- }
395
- }
396
-
397
- return Effect.succeed(sparse)
398
- }
399
- case "sparse-class-name": {
400
- const parts: Array<ClassNamePart | StaticText> = []
401
- const values: Array<string | Array<string>> = [] // TODO: Do this for all other sparse attrs
402
- const sparse = SparseClassNamePartImpl.browser(
403
- parts,
404
- node as HTMLElement | SVGElement,
405
- ctx,
406
- values
407
- )
408
-
409
- for (let i = 0; i < part.nodes.length; ++i) {
410
- const node = part.nodes[i]
411
-
412
- if (node._tag === "text") {
413
- parts.push(new StaticTextImpl(node.value))
414
- values.push(node.value)
415
- } else {
416
- values.push([])
417
- parts.push(
418
- new ClassNamePartImpl(
419
- node.index,
420
- ({ value }) => sparse.update(replace(sparse.value, i, value || "")),
421
- []
422
- )
423
- )
424
- }
425
- }
426
-
427
- return Effect.succeed(sparse)
428
- }
429
- case "sparse-comment": {
430
- const parts: Array<CommentPart | StaticText> = Array(part.nodes.length)
431
- const sparse = SparseCommentPartImpl.browser(
432
- node as Comment,
433
- parts,
434
- ctx
435
- )
436
-
437
- for (let i = 0; i < part.nodes.length; ++i) {
438
- const node = part.nodes[i]
439
-
440
- if (node._tag === "text") {
441
- parts.push(new StaticTextImpl(node.value))
442
- ;(sparse as any).value[i] = node.value
443
- } else {
444
- parts.push(
445
- new CommentPartImpl(
446
- node.index,
447
- ({ value }) => sparse.update(replace(sparse.value, i, value || "")),
448
- sparse.value[i]
449
- )
450
- )
451
- }
452
- }
453
-
454
- return Effect.succeed(sparse)
455
- }
456
- case "text-part":
457
- return Effect.succeed(TextPartImpl.browser(document, part.index, node as Element, ctx))
458
- }
459
- }
460
-
461
725
  export function buildTemplate(document: Document, { nodes }: Template.Template): DocumentFragment {
462
726
  const fragment = document.createDocumentFragment()
463
727
 
@@ -484,6 +748,39 @@ function buildNode(document: Document, node: Template.Node, isSvgContext: boolea
484
748
  case "comment-part":
485
749
  case "node":
486
750
  return document.createComment(`hole${node.index}`)
751
+ case "doctype":
752
+ return document.implementation.createDocumentType(
753
+ node.name,
754
+ docTypeNameToPublicId(node.name),
755
+ docTypeNameToSystemId(node.name)
756
+ )
757
+ }
758
+ }
759
+
760
+ function docTypeNameToPublicId(name: string): string {
761
+ switch (name) {
762
+ case "html":
763
+ return "-//W3C//DTD HTML 4.01//EN"
764
+ case "svg":
765
+ return "-//W3C//DTD SVG 1.1//EN"
766
+ case "math":
767
+ return "-//W3C//DTD MathML 2.0//EN"
768
+ default:
769
+ return ""
770
+ }
771
+ }
772
+
773
+ function docTypeNameToSystemId(name: string): string {
774
+ switch (name) {
775
+ // HTML5
776
+ case "html":
777
+ return "http://www.w3.org/TR/html4/strict.dtd"
778
+ case "svg":
779
+ return "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
780
+ case "math":
781
+ return "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd"
782
+ default:
783
+ return ""
487
784
  }
488
785
  }
489
786
 
@@ -527,3 +824,55 @@ function buildTextChild(document: Document, node: Template.Text): globalThis.Nod
527
824
 
528
825
  return document.createComment(`hole${node.index}`)
529
826
  }
827
+
828
+ function createAttribute(
829
+ document: Document,
830
+ element: HTMLElement | SVGElement,
831
+ name: string
832
+ ): Attr {
833
+ return element.getAttributeNode(name) ?? document.createAttribute(name)
834
+ }
835
+
836
+ function matchSettablePart(
837
+ renderable: Renderable<any, any>,
838
+ setValue: (value: any) => void,
839
+ makePart: () => Part,
840
+ schedule: (f: () => void) => Effect.Effect<Scope, never, void>,
841
+ expect: () => void
842
+ ) {
843
+ return matchRenderable(renderable, {
844
+ Fx: (fx) => {
845
+ expect()
846
+ return Fx.observe(fx, (a) => schedule(() => setValue(a)))
847
+ },
848
+ Effect: (effect) => {
849
+ expect()
850
+ return Effect.flatMap(effect, (a) => schedule(() => setValue(a)))
851
+ },
852
+ Directive: (directive) => {
853
+ expect()
854
+ return directive(makePart())
855
+ },
856
+ Otherwise: (otherwise) => {
857
+ setValue(otherwise)
858
+ return null
859
+ }
860
+ })
861
+ }
862
+
863
+ function matchRenderable(renderable: Renderable<any, any>, matches: {
864
+ Fx: (fx: Fx.Fx<any, any, any>) => Effect.Effect<any, any, void> | null
865
+ Effect: (effect: Effect.Effect<any, any, any>) => Effect.Effect<any, any, void> | null
866
+ Directive: (directive: Directive<any, any>) => Effect.Effect<any, any, void> | null
867
+ Otherwise: (_: Renderable<any, any>) => Effect.Effect<any, any, void> | null
868
+ }): Effect.Effect<any, any, void> | null {
869
+ if (Fx.isFx(renderable)) {
870
+ return matches.Fx(renderable)
871
+ } else if (Effect.isEffect(renderable)) {
872
+ return matches.Effect(renderable)
873
+ } else if (isDirective<any, any>(renderable)) {
874
+ return matches.Directive(renderable)
875
+ } else {
876
+ return matches.Otherwise(renderable)
877
+ }
878
+ }