@typed/template 0.1.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 (285) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/Directive.js +76 -0
  4. package/dist/cjs/Directive.js.map +1 -0
  5. package/dist/cjs/ElementRef.js +83 -0
  6. package/dist/cjs/ElementRef.js.map +1 -0
  7. package/dist/cjs/ElementSource.js +244 -0
  8. package/dist/cjs/ElementSource.js.map +1 -0
  9. package/dist/cjs/Entry.js +6 -0
  10. package/dist/cjs/Entry.js.map +1 -0
  11. package/dist/cjs/EventHandler.js +65 -0
  12. package/dist/cjs/EventHandler.js.map +1 -0
  13. package/dist/cjs/Html.js +169 -0
  14. package/dist/cjs/Html.js.map +1 -0
  15. package/dist/cjs/HtmlChunk.js +257 -0
  16. package/dist/cjs/HtmlChunk.js.map +1 -0
  17. package/dist/cjs/Hydrate.js +49 -0
  18. package/dist/cjs/Hydrate.js.map +1 -0
  19. package/dist/cjs/Many.js +45 -0
  20. package/dist/cjs/Many.js.map +1 -0
  21. package/dist/cjs/Meta.js +37 -0
  22. package/dist/cjs/Meta.js.map +1 -0
  23. package/dist/cjs/Parser.js +331 -0
  24. package/dist/cjs/Parser.js.map +1 -0
  25. package/dist/cjs/Part.js +6 -0
  26. package/dist/cjs/Part.js.map +1 -0
  27. package/dist/cjs/Placeholder.js +38 -0
  28. package/dist/cjs/Placeholder.js.map +1 -0
  29. package/dist/cjs/Platform.js +64 -0
  30. package/dist/cjs/Platform.js.map +1 -0
  31. package/dist/cjs/Render.js +39 -0
  32. package/dist/cjs/Render.js.map +1 -0
  33. package/dist/cjs/RenderContext.js +130 -0
  34. package/dist/cjs/RenderContext.js.map +1 -0
  35. package/dist/cjs/RenderEvent.js +44 -0
  36. package/dist/cjs/RenderEvent.js.map +1 -0
  37. package/dist/cjs/RenderTemplate.js +41 -0
  38. package/dist/cjs/RenderTemplate.js.map +1 -0
  39. package/dist/cjs/Renderable.js +6 -0
  40. package/dist/cjs/Renderable.js.map +1 -0
  41. package/dist/cjs/Template.js +266 -0
  42. package/dist/cjs/Template.js.map +1 -0
  43. package/dist/cjs/TemplateInstance.js +51 -0
  44. package/dist/cjs/TemplateInstance.js.map +1 -0
  45. package/dist/cjs/Test.js +90 -0
  46. package/dist/cjs/Test.js.map +1 -0
  47. package/dist/cjs/Token.js +270 -0
  48. package/dist/cjs/Token.js.map +1 -0
  49. package/dist/cjs/Tokenizer.js +18 -0
  50. package/dist/cjs/Tokenizer.js.map +1 -0
  51. package/dist/cjs/Vitest.js +44 -0
  52. package/dist/cjs/Vitest.js.map +1 -0
  53. package/dist/cjs/index.js +147 -0
  54. package/dist/cjs/index.js.map +1 -0
  55. package/dist/cjs/internal/HydrateContext.js +13 -0
  56. package/dist/cjs/internal/HydrateContext.js.map +1 -0
  57. package/dist/cjs/internal/browser.js +109 -0
  58. package/dist/cjs/internal/browser.js.map +1 -0
  59. package/dist/cjs/internal/chunks.js +54 -0
  60. package/dist/cjs/internal/chunks.js.map +1 -0
  61. package/dist/cjs/internal/errors.js +23 -0
  62. package/dist/cjs/internal/errors.js.map +1 -0
  63. package/dist/cjs/internal/hydrate.js +197 -0
  64. package/dist/cjs/internal/hydrate.js.map +1 -0
  65. package/dist/cjs/internal/indexRefCounter.js +32 -0
  66. package/dist/cjs/internal/indexRefCounter.js.map +1 -0
  67. package/dist/cjs/internal/module-augmentation.js +6 -0
  68. package/dist/cjs/internal/module-augmentation.js.map +1 -0
  69. package/dist/cjs/internal/parser.js +492 -0
  70. package/dist/cjs/internal/parser.js.map +1 -0
  71. package/dist/cjs/internal/parts.js +350 -0
  72. package/dist/cjs/internal/parts.js.map +1 -0
  73. package/dist/cjs/internal/readAttribute.js +34 -0
  74. package/dist/cjs/internal/readAttribute.js.map +1 -0
  75. package/dist/cjs/internal/render.js +332 -0
  76. package/dist/cjs/internal/render.js.map +1 -0
  77. package/dist/cjs/internal/server.js +219 -0
  78. package/dist/cjs/internal/server.js.map +1 -0
  79. package/dist/cjs/internal/tokenizer.js +264 -0
  80. package/dist/cjs/internal/tokenizer.js.map +1 -0
  81. package/dist/cjs/internal/utils.js +68 -0
  82. package/dist/cjs/internal/utils.js.map +1 -0
  83. package/dist/dts/Directive.d.ts +70 -0
  84. package/dist/dts/Directive.d.ts.map +1 -0
  85. package/dist/dts/ElementRef.d.ts +40 -0
  86. package/dist/dts/ElementRef.d.ts.map +1 -0
  87. package/dist/dts/ElementSource.d.ts +72 -0
  88. package/dist/dts/ElementSource.d.ts.map +1 -0
  89. package/dist/dts/Entry.d.ts +26 -0
  90. package/dist/dts/Entry.d.ts.map +1 -0
  91. package/dist/dts/EventHandler.d.ts +61 -0
  92. package/dist/dts/EventHandler.d.ts.map +1 -0
  93. package/dist/dts/Html.d.ts +17 -0
  94. package/dist/dts/Html.d.ts.map +1 -0
  95. package/dist/dts/HtmlChunk.d.ts +56 -0
  96. package/dist/dts/HtmlChunk.d.ts.map +1 -0
  97. package/dist/dts/Hydrate.d.ts +20 -0
  98. package/dist/dts/Hydrate.d.ts.map +1 -0
  99. package/dist/dts/Many.d.ts +32 -0
  100. package/dist/dts/Many.d.ts.map +1 -0
  101. package/dist/dts/Meta.d.ts +24 -0
  102. package/dist/dts/Meta.d.ts.map +1 -0
  103. package/dist/dts/Parser.d.ts +16 -0
  104. package/dist/dts/Parser.d.ts.map +1 -0
  105. package/dist/dts/Part.d.ts +147 -0
  106. package/dist/dts/Part.d.ts.map +1 -0
  107. package/dist/dts/Placeholder.d.ts +51 -0
  108. package/dist/dts/Placeholder.d.ts.map +1 -0
  109. package/dist/dts/Platform.d.ts +23 -0
  110. package/dist/dts/Platform.d.ts.map +1 -0
  111. package/dist/dts/Render.d.ts +23 -0
  112. package/dist/dts/Render.d.ts.map +1 -0
  113. package/dist/dts/RenderContext.d.ts +88 -0
  114. package/dist/dts/RenderContext.d.ts.map +1 -0
  115. package/dist/dts/RenderEvent.d.ts +37 -0
  116. package/dist/dts/RenderEvent.d.ts.map +1 -0
  117. package/dist/dts/RenderTemplate.d.ts +38 -0
  118. package/dist/dts/RenderTemplate.d.ts.map +1 -0
  119. package/dist/dts/Renderable.d.ts +28 -0
  120. package/dist/dts/Renderable.d.ts.map +1 -0
  121. package/dist/dts/Template.d.ts +218 -0
  122. package/dist/dts/Template.d.ts.map +1 -0
  123. package/dist/dts/TemplateInstance.d.ts +32 -0
  124. package/dist/dts/TemplateInstance.d.ts.map +1 -0
  125. package/dist/dts/Test.d.ts +58 -0
  126. package/dist/dts/Test.d.ts.map +1 -0
  127. package/dist/dts/Token.d.ts +202 -0
  128. package/dist/dts/Token.d.ts.map +1 -0
  129. package/dist/dts/Tokenizer.d.ts +6 -0
  130. package/dist/dts/Tokenizer.d.ts.map +1 -0
  131. package/dist/dts/Vitest.d.ts +28 -0
  132. package/dist/dts/Vitest.d.ts.map +1 -0
  133. package/dist/dts/index.d.ts +65 -0
  134. package/dist/dts/index.d.ts.map +1 -0
  135. package/dist/dts/internal/HydrateContext.d.ts +2 -0
  136. package/dist/dts/internal/HydrateContext.d.ts.map +1 -0
  137. package/dist/dts/internal/browser.d.ts +8 -0
  138. package/dist/dts/internal/browser.d.ts.map +1 -0
  139. package/dist/dts/internal/chunks.d.ts +22 -0
  140. package/dist/dts/internal/chunks.d.ts.map +1 -0
  141. package/dist/dts/internal/errors.d.ts +9 -0
  142. package/dist/dts/internal/errors.d.ts.map +1 -0
  143. package/dist/dts/internal/hydrate.d.ts +37 -0
  144. package/dist/dts/internal/hydrate.d.ts.map +1 -0
  145. package/dist/dts/internal/indexRefCounter.d.ts +6 -0
  146. package/dist/dts/internal/indexRefCounter.d.ts.map +1 -0
  147. package/dist/dts/internal/module-augmentation.d.ts +36 -0
  148. package/dist/dts/internal/module-augmentation.d.ts.map +1 -0
  149. package/dist/dts/internal/parser.d.ts +12 -0
  150. package/dist/dts/internal/parser.d.ts.map +1 -0
  151. package/dist/dts/internal/parts.d.ts +304 -0
  152. package/dist/dts/internal/parts.d.ts.map +1 -0
  153. package/dist/dts/internal/readAttribute.d.ts +9 -0
  154. package/dist/dts/internal/readAttribute.d.ts.map +1 -0
  155. package/dist/dts/internal/render.d.ts +30 -0
  156. package/dist/dts/internal/render.d.ts.map +1 -0
  157. package/dist/dts/internal/server.d.ts +31 -0
  158. package/dist/dts/internal/server.d.ts.map +1 -0
  159. package/dist/dts/internal/tokenizer.d.ts +3 -0
  160. package/dist/dts/internal/tokenizer.d.ts.map +1 -0
  161. package/dist/dts/internal/utils.d.ts +15 -0
  162. package/dist/dts/internal/utils.d.ts.map +1 -0
  163. package/dist/esm/Directive.js +64 -0
  164. package/dist/esm/Directive.js.map +1 -0
  165. package/dist/esm/ElementRef.js +72 -0
  166. package/dist/esm/ElementRef.js.map +1 -0
  167. package/dist/esm/ElementSource.js +237 -0
  168. package/dist/esm/ElementSource.js.map +1 -0
  169. package/dist/esm/Entry.js +2 -0
  170. package/dist/esm/Entry.js.map +1 -0
  171. package/dist/esm/EventHandler.js +52 -0
  172. package/dist/esm/EventHandler.js.map +1 -0
  173. package/dist/esm/Html.js +167 -0
  174. package/dist/esm/Html.js.map +1 -0
  175. package/dist/esm/HtmlChunk.js +274 -0
  176. package/dist/esm/HtmlChunk.js.map +1 -0
  177. package/dist/esm/Hydrate.js +37 -0
  178. package/dist/esm/Hydrate.js.map +1 -0
  179. package/dist/esm/Many.js +33 -0
  180. package/dist/esm/Many.js.map +1 -0
  181. package/dist/esm/Meta.js +29 -0
  182. package/dist/esm/Meta.js.map +1 -0
  183. package/dist/esm/Parser.js +342 -0
  184. package/dist/esm/Parser.js.map +1 -0
  185. package/dist/esm/Part.js +5 -0
  186. package/dist/esm/Part.js.map +1 -0
  187. package/dist/esm/Placeholder.js +30 -0
  188. package/dist/esm/Placeholder.js.map +1 -0
  189. package/dist/esm/Platform.js +41 -0
  190. package/dist/esm/Platform.js.map +1 -0
  191. package/dist/esm/Render.js +27 -0
  192. package/dist/esm/Render.js.map +1 -0
  193. package/dist/esm/RenderContext.js +113 -0
  194. package/dist/esm/RenderContext.js.map +1 -0
  195. package/dist/esm/RenderEvent.js +36 -0
  196. package/dist/esm/RenderEvent.js.map +1 -0
  197. package/dist/esm/RenderTemplate.js +26 -0
  198. package/dist/esm/RenderTemplate.js.map +1 -0
  199. package/dist/esm/Renderable.js +2 -0
  200. package/dist/esm/Renderable.js.map +1 -0
  201. package/dist/esm/Template.js +239 -0
  202. package/dist/esm/Template.js.map +1 -0
  203. package/dist/esm/TemplateInstance.js +43 -0
  204. package/dist/esm/TemplateInstance.js.map +1 -0
  205. package/dist/esm/Test.js +68 -0
  206. package/dist/esm/Test.js.map +1 -0
  207. package/dist/esm/Token.js +264 -0
  208. package/dist/esm/Token.js.map +1 -0
  209. package/dist/esm/Tokenizer.js +9 -0
  210. package/dist/esm/Tokenizer.js.map +1 -0
  211. package/dist/esm/Vitest.js +29 -0
  212. package/dist/esm/Vitest.js.map +1 -0
  213. package/dist/esm/index.js +65 -0
  214. package/dist/esm/index.js.map +1 -0
  215. package/dist/esm/internal/HydrateContext.js +7 -0
  216. package/dist/esm/internal/HydrateContext.js.map +1 -0
  217. package/dist/esm/internal/browser.js +102 -0
  218. package/dist/esm/internal/browser.js.map +1 -0
  219. package/dist/esm/internal/chunks.js +47 -0
  220. package/dist/esm/internal/chunks.js.map +1 -0
  221. package/dist/esm/internal/errors.js +15 -0
  222. package/dist/esm/internal/errors.js.map +1 -0
  223. package/dist/esm/internal/hydrate.js +165 -0
  224. package/dist/esm/internal/hydrate.js.map +1 -0
  225. package/dist/esm/internal/indexRefCounter.js +24 -0
  226. package/dist/esm/internal/indexRefCounter.js.map +1 -0
  227. package/dist/esm/internal/module-augmentation.js +2 -0
  228. package/dist/esm/internal/module-augmentation.js.map +1 -0
  229. package/dist/esm/internal/parser.js +493 -0
  230. package/dist/esm/internal/parser.js.map +1 -0
  231. package/dist/esm/internal/parts.js +291 -0
  232. package/dist/esm/internal/parts.js.map +1 -0
  233. package/dist/esm/internal/readAttribute.js +24 -0
  234. package/dist/esm/internal/readAttribute.js.map +1 -0
  235. package/dist/esm/internal/render.js +329 -0
  236. package/dist/esm/internal/render.js.map +1 -0
  237. package/dist/esm/internal/server.js +174 -0
  238. package/dist/esm/internal/server.js.map +1 -0
  239. package/dist/esm/internal/tokenizer.js +296 -0
  240. package/dist/esm/internal/tokenizer.js.map +1 -0
  241. package/dist/esm/internal/utils.js +52 -0
  242. package/dist/esm/internal/utils.js.map +1 -0
  243. package/dist/esm/package.json +4 -0
  244. package/package.json +242 -0
  245. package/src/Directive.ts +114 -0
  246. package/src/ElementRef.ts +123 -0
  247. package/src/ElementSource.ts +417 -0
  248. package/src/Entry.ts +28 -0
  249. package/src/EventHandler.ts +104 -0
  250. package/src/Html.ts +258 -0
  251. package/src/HtmlChunk.ts +346 -0
  252. package/src/Hydrate.ts +53 -0
  253. package/src/Many.ts +128 -0
  254. package/src/Meta.ts +32 -0
  255. package/src/Parser.ts +457 -0
  256. package/src/Part.ts +186 -0
  257. package/src/Placeholder.ts +70 -0
  258. package/src/Platform.ts +71 -0
  259. package/src/Render.ts +45 -0
  260. package/src/RenderContext.ts +221 -0
  261. package/src/RenderEvent.ts +67 -0
  262. package/src/RenderTemplate.ts +88 -0
  263. package/src/Renderable.ts +34 -0
  264. package/src/Template.ts +284 -0
  265. package/src/TemplateInstance.ts +83 -0
  266. package/src/Test.ts +151 -0
  267. package/src/Token.ts +269 -0
  268. package/src/Tokenizer.ts +10 -0
  269. package/src/Vitest.ts +61 -0
  270. package/src/index.ts +66 -0
  271. package/src/internal/HydrateContext.ts +23 -0
  272. package/src/internal/browser.ts +132 -0
  273. package/src/internal/chunks.ts +73 -0
  274. package/src/internal/errors.ts +11 -0
  275. package/src/internal/external.d.ts +11 -0
  276. package/src/internal/hydrate.ts +262 -0
  277. package/src/internal/indexRefCounter.ts +33 -0
  278. package/src/internal/module-augmentation.ts +48 -0
  279. package/src/internal/parser.ts +637 -0
  280. package/src/internal/parts.ts +527 -0
  281. package/src/internal/readAttribute.ts +28 -0
  282. package/src/internal/render.ts +529 -0
  283. package/src/internal/server.ts +293 -0
  284. package/src/internal/tokenizer.ts +338 -0
  285. package/src/internal/utils.ts +73 -0
@@ -0,0 +1,527 @@
1
+ import type { Context } from "@typed/context"
2
+ import * as Fx from "@typed/fx/Fx"
3
+ import { WithContext } from "@typed/fx/Sink"
4
+ import { isText, type Rendered } from "@typed/wire"
5
+ import type { Cause } from "effect/Cause"
6
+ import * as Effect from "effect/Effect"
7
+ import { equals } from "effect/Equal"
8
+ import { strict } from "effect/Equivalence"
9
+ import type { Equivalence } from "effect/Equivalence"
10
+ import * as Fiber from "effect/Fiber"
11
+ import * as ReadonlyArray from "effect/ReadonlyArray"
12
+ import type { Scope } from "effect/Scope"
13
+ import * as SynchronizedRef from "effect/SynchronizedRef"
14
+ import type { ElementRef } from "../ElementRef"
15
+ import type { ElementSource } from "../ElementSource"
16
+ import { unescape } from "../HtmlChunk"
17
+ import type {
18
+ AttributePart,
19
+ BooleanPart,
20
+ ClassNamePart,
21
+ CommentPart,
22
+ DataPart,
23
+ EventPart,
24
+ NodePart,
25
+ Part,
26
+ PropertyPart,
27
+ RefPart,
28
+ SparseAttributePart,
29
+ SparseClassNamePart,
30
+ SparseCommentPart,
31
+ SparsePart,
32
+ StaticText,
33
+ TextPart
34
+ } from "../Part"
35
+ import type { RenderContext } from "../RenderContext"
36
+ import { findHoleComment } from "./utils"
37
+
38
+ const strictEq = strict<any>()
39
+
40
+ const base = <T extends Part["_tag"]>(tag: T) =>
41
+ class Base {
42
+ readonly _tag: T = tag
43
+
44
+ constructor(
45
+ readonly index: number,
46
+ readonly commit: (
47
+ params: {
48
+ previous: Extract<Part, { readonly _tag: T }>["value"]
49
+ value: Extract<Part, { readonly _tag: T }>["value"]
50
+ part: Extract<Part, { readonly _tag: T }>
51
+ }
52
+ ) => Effect.Effect<Scope, never, void>,
53
+ public value: Extract<Part, { readonly _tag: T }>["value"],
54
+ readonly eq: Equivalence<Extract<Part, { readonly _tag: T }>["value"]> = equals
55
+ ) {
56
+ this.update = this.update.bind(this)
57
+ }
58
+
59
+ update(input: this["value"]) {
60
+ const previous = this.value as any
61
+ const value = this.getValue(input) as any
62
+
63
+ if (this.eq(previous as any, value as any)) {
64
+ return Effect.unit
65
+ }
66
+
67
+ return Effect.tap(
68
+ this.commit.call(this, {
69
+ previous,
70
+ value,
71
+ part: this as any
72
+ }),
73
+ () => Effect.sync(() => this.value = value)
74
+ )
75
+ }
76
+
77
+ getValue(value: unknown) {
78
+ return value
79
+ }
80
+ }
81
+
82
+ export class AttributePartImpl extends base("attribute") implements AttributePart {
83
+ constructor(
84
+ readonly name: string,
85
+ index: number,
86
+ commit: AttributePartImpl["commit"],
87
+ value: AttributePart["value"]
88
+ ) {
89
+ super(index, commit, value, strictEq)
90
+ }
91
+
92
+ static browser(index: number, element: Element, name: string, context: RenderContext): AttributePartImpl {
93
+ return new AttributePartImpl(
94
+ name,
95
+ index,
96
+ ({ part, value }) =>
97
+ context.queue.add(
98
+ part,
99
+ () => value == null ? element.removeAttribute(name) : element.setAttribute(name, value)
100
+ ),
101
+ element.getAttribute(name)
102
+ )
103
+ }
104
+
105
+ static server(
106
+ name: string,
107
+ index: number,
108
+ commit: AttributePartImpl["commit"]
109
+ ) {
110
+ return new AttributePartImpl(name, index, commit, null)
111
+ }
112
+ }
113
+
114
+ export class BooleanPartImpl extends base("boolean") implements BooleanPart {
115
+ constructor(
116
+ readonly name: string,
117
+ index: number,
118
+ commit: BooleanPartImpl["commit"],
119
+ value: BooleanPart["value"]
120
+ ) {
121
+ super(index, commit, value, strictEq)
122
+ }
123
+
124
+ static browser(index: number, element: Element, name: string, context: RenderContext): BooleanPartImpl {
125
+ return new BooleanPartImpl(
126
+ name,
127
+ index,
128
+ ({ part, value }) =>
129
+ context.queue.add(
130
+ part,
131
+ () => element.toggleAttribute(name, value === true)
132
+ ),
133
+ element.hasAttribute(name)
134
+ )
135
+ }
136
+
137
+ static server(
138
+ name: string,
139
+ index: number,
140
+ commit: BooleanPartImpl["commit"]
141
+ ) {
142
+ return new BooleanPartImpl(name, index, commit, null)
143
+ }
144
+ }
145
+
146
+ const isString = (x: unknown): x is string => typeof x === "string"
147
+
148
+ export class ClassNamePartImpl extends base("className") implements ClassNamePart {
149
+ constructor(
150
+ index: number,
151
+ commit: ClassNamePartImpl["commit"],
152
+ value: ClassNamePart["value"]
153
+ ) {
154
+ super(index, commit, value, strictEq)
155
+ }
156
+
157
+ getValue(value: unknown): ReadonlyArray<string> {
158
+ if (isString(value)) {
159
+ return value.split(" ").filter((x) => isString(x) && x.trim() !== "")
160
+ }
161
+
162
+ if (Array.isArray(value)) {
163
+ return value.filter((x) => isString(x) && x.trim() !== "")
164
+ }
165
+
166
+ return []
167
+ }
168
+
169
+ static browser(index: number, element: Element, context: RenderContext): ClassNamePartImpl {
170
+ return new ClassNamePartImpl(
171
+ index,
172
+ ({ part, previous, value }) =>
173
+ context.queue.add(
174
+ part,
175
+ () => {
176
+ const { added, removed } = diffStrings(
177
+ previous,
178
+ value
179
+ )
180
+
181
+ element.classList.add(...added)
182
+ element.classList.remove(...removed)
183
+ }
184
+ ),
185
+ Array.from(element.classList)
186
+ )
187
+ }
188
+
189
+ static server(
190
+ index: number,
191
+ commit: ClassNamePartImpl["commit"]
192
+ ) {
193
+ return new ClassNamePartImpl(index, commit, null)
194
+ }
195
+ }
196
+
197
+ function diffStrings(
198
+ previous: ReadonlyArray<string> | null | undefined,
199
+ current: ReadonlyArray<string> | null | undefined
200
+ ): { added: ReadonlyArray<string>; removed: ReadonlyArray<string>; unchanged: ReadonlyArray<string> } {
201
+ if (previous == null || previous.length === 0) {
202
+ return {
203
+ added: current || [],
204
+ removed: [],
205
+ unchanged: []
206
+ }
207
+ } else if (current == null || current.length === 0) {
208
+ return {
209
+ added: [],
210
+ removed: previous,
211
+ unchanged: []
212
+ }
213
+ } else {
214
+ const added = current.filter((c) => !previous.includes(c))
215
+ const removed: Array<string> = []
216
+ const unchanged: Array<string> = []
217
+
218
+ for (let i = 0; i < previous.length; ++i) {
219
+ if (current.includes(previous[i])) {
220
+ unchanged.push(previous[i])
221
+ } else {
222
+ removed.push(previous[i])
223
+ }
224
+ }
225
+
226
+ return {
227
+ added,
228
+ removed,
229
+ unchanged
230
+ }
231
+ }
232
+ }
233
+
234
+ export class CommentPartImpl extends base("comment") implements CommentPart {
235
+ static browser(index: number, comment: globalThis.Comment, ctx: RenderContext) {
236
+ return new CommentPartImpl(
237
+ index,
238
+ ({ part, value }) => ctx.queue.add(part, () => comment.data = value || ""),
239
+ comment.data,
240
+ strictEq
241
+ )
242
+ }
243
+
244
+ static server(index: number, commit: CommentPartImpl["commit"]) {
245
+ return new CommentPartImpl(index, commit, null)
246
+ }
247
+ }
248
+
249
+ export class DataPartImpl extends base("data") implements DataPart {
250
+ static browser(index: number, element: HTMLElement | SVGElement, ctx: RenderContext) {
251
+ return new DataPartImpl(
252
+ index,
253
+ ({ part, previous, value }) =>
254
+ ctx.queue.add(
255
+ part,
256
+ () => {
257
+ const diff = diffDataSet(previous, value)
258
+
259
+ if (diff) {
260
+ const { added, removed } = diff
261
+
262
+ removed.forEach((k) => delete element.dataset[k])
263
+ added.forEach(([k, v]) => element.dataset[k] = v)
264
+ }
265
+ }
266
+ ),
267
+ element.dataset
268
+ )
269
+ }
270
+
271
+ static server(index: number, commit: DataPartImpl["commit"]) {
272
+ return new DataPartImpl(index, commit, null)
273
+ }
274
+ }
275
+
276
+ function diffDataSet(
277
+ a: Record<string, string | undefined> | null | undefined,
278
+ b: Record<string, string | undefined> | null | undefined
279
+ ):
280
+ | { added: Array<readonly [string, string | undefined]>; removed: ReadonlyArray<string> }
281
+ | null
282
+ {
283
+ if (!a) return b ? { added: Object.entries(b), removed: [] } : null
284
+ if (!b) return { added: [], removed: Object.keys(a) }
285
+
286
+ const { added, removed, unchanged } = diffStrings(Object.keys(a), Object.keys(b))
287
+
288
+ return {
289
+ added: added.concat(unchanged).map((k) => [k, b[k]] as const),
290
+ removed
291
+ }
292
+ }
293
+
294
+ export class EventPartImpl extends base("event") implements EventPart {
295
+ constructor(
296
+ readonly name: string,
297
+ readonly onCause: <E>(cause: Cause<E>) => Effect.Effect<never, never, unknown>,
298
+ index: number,
299
+ commit: EventPartImpl["commit"],
300
+ value: EventPart["value"]
301
+ ) {
302
+ super(index, commit, value, strictEq)
303
+ }
304
+
305
+ static browser<T extends Rendered, E>(
306
+ name: string,
307
+ index: number,
308
+ ref: ElementRef<T>,
309
+ element: HTMLElement | SVGElement,
310
+ onCause: (cause: Cause<E>) => Effect.Effect<never, never, unknown>
311
+ ): Effect.Effect<unknown, never, void> {
312
+ return withSwitchFork((fork, ctx) => {
313
+ const source = ref.query(element)
314
+
315
+ return Effect.succeed(
316
+ new EventPartImpl(
317
+ name,
318
+ onCause as any,
319
+ index,
320
+ ({ value }) => {
321
+ return value
322
+ ? Fx.run(
323
+ source.events(name as keyof HTMLElementEventMap | keyof SVGElementEventMap, value.options),
324
+ WithContext(onCause, value.handler)
325
+ ).pipe(
326
+ Effect.provide(ctx),
327
+ fork
328
+ )
329
+ : fork(Effect.unit)
330
+ },
331
+ null
332
+ )
333
+ )
334
+ })
335
+ }
336
+ }
337
+
338
+ function withScopedFork<R, E, A>(f: (fork: Fx.ScopedFork) => Effect.Effect<R, E, A>): Effect.Effect<R | Scope, E, A> {
339
+ return Effect.scopeWith((scope) => f(Effect.forkIn(scope)))
340
+ }
341
+
342
+ // Ensures only a single fiber is executing
343
+ function withSwitchFork<R, E, A>(
344
+ f: (fork: Fx.FxFork, ctx: Context<R | Scope>) => Effect.Effect<R, E, A>
345
+ ): Effect.Effect<R | Scope, E, A> {
346
+ return Effect.contextWithEffect((ctx) =>
347
+ withScopedFork((fork) =>
348
+ Effect.flatMap(
349
+ SynchronizedRef.make<Fiber.Fiber<never, void>>(Fiber.unit),
350
+ (ref) =>
351
+ f((effect) =>
352
+ SynchronizedRef.updateAndGetEffect(
353
+ ref,
354
+ (fiber) => Effect.flatMap(Fiber.interrupt(fiber), () => fork(effect))
355
+ ), ctx)
356
+ )
357
+ )
358
+ )
359
+ }
360
+
361
+ export class NodePartImpl extends base("node") implements NodePart {}
362
+
363
+ export class PropertyPartImpl extends base("property") implements PropertyPart {
364
+ constructor(
365
+ readonly name: string,
366
+ index: number,
367
+ commit: PropertyPartImpl["commit"],
368
+ value: PropertyPartImpl["value"]
369
+ ) {
370
+ super(index, commit, value, strictEq)
371
+ }
372
+
373
+ static browser(index: number, node: Node, name: string, ctx: RenderContext) {
374
+ const existing = (node as Element).getAttribute(name)
375
+
376
+ return new PropertyPartImpl(
377
+ name,
378
+ index,
379
+ ({ part, value }) => ctx.queue.add(part, () => (node as any)[name] = value),
380
+ existing ? unescape(existing) : null
381
+ )
382
+ }
383
+ }
384
+
385
+ export class RefPartImpl implements RefPart {
386
+ readonly _tag = "ref"
387
+
388
+ constructor(readonly value: ElementSource<any>, readonly index: number) {}
389
+ }
390
+
391
+ export class TextPartImpl extends base("text") implements TextPart {
392
+ // TODO: Make this properly
393
+ static browser(document: Document, index: number, element: Element, ctx: RenderContext) {
394
+ const comment = findHoleComment(element, index)
395
+ const text = comment.previousSibling && isText(comment.previousSibling)
396
+ ? comment.previousSibling
397
+ : document.createTextNode("")
398
+
399
+ return new TextPartImpl(
400
+ index,
401
+ ({ part, value }) => ctx.queue.add(part, () => text.nodeValue = value ?? null),
402
+ text.nodeValue,
403
+ strictEq
404
+ )
405
+ }
406
+ }
407
+
408
+ const sparse = <T extends SparsePart["_tag"]>(tag: T) =>
409
+ class Base {
410
+ readonly _tag: T = tag
411
+
412
+ constructor(
413
+ readonly commit: (
414
+ params: {
415
+ previous: SparseAttributeValues<Extract<SparsePart, { readonly _tag: T }>["parts"]>
416
+ value: SparseAttributeValues<Extract<SparsePart, { readonly _tag: T }>["parts"]>
417
+ part: Extract<SparsePart, { readonly _tag: T }>
418
+ }
419
+ ) => Effect.Effect<Scope, never, void>,
420
+ public value: SparseAttributeValues<Extract<SparsePart, { readonly _tag: T }>["parts"]>,
421
+ readonly eq: Equivalence<SparseAttributeValues<Extract<SparsePart, { readonly _tag: T }>["parts"]>> = equals
422
+ ) {}
423
+
424
+ update = (value: this["value"]) => {
425
+ if (this.eq(this.value as any, value as any)) {
426
+ return Effect.unit
427
+ }
428
+
429
+ return this.commit({
430
+ previous: this.value,
431
+ value: this.value = value as any,
432
+ part: this
433
+ } as any)
434
+ }
435
+ }
436
+
437
+ type SparseAttributeValues<T extends ReadonlyArray<AttributePart | ClassNamePart | CommentPart | StaticText>> =
438
+ ReadonlyArray<
439
+ SparseAttributeValue<T[number]>
440
+ >
441
+ type SparseAttributeValue<T extends AttributePart | ClassNamePart | CommentPart | StaticText> = T["value"]
442
+
443
+ export class SparseAttributePartImpl extends sparse("sparse/attribute") implements SparseAttributePart {
444
+ constructor(
445
+ readonly name: string,
446
+ readonly parts: ReadonlyArray<AttributePart | StaticText>,
447
+ commit: SparseAttributePartImpl["commit"]
448
+ ) {
449
+ super(commit, [], ReadonlyArray.getEquivalence(strictEq))
450
+ }
451
+
452
+ static browser(
453
+ name: string,
454
+ parts: ReadonlyArray<AttributePart | StaticText>,
455
+ element: HTMLElement | SVGElement,
456
+ ctx: RenderContext
457
+ ) {
458
+ return new SparseAttributePartImpl(
459
+ name,
460
+ parts,
461
+ ({ part, value }) =>
462
+ ctx.queue.add(part, () => element.setAttribute(name, value.flatMap((s) => isNonEmptyString(s, true)).join("")))
463
+ )
464
+ }
465
+ }
466
+
467
+ export class SparseClassNamePartImpl extends sparse("sparse/className") implements SparseClassNamePart {
468
+ constructor(
469
+ readonly parts: ReadonlyArray<ClassNamePart | StaticText>,
470
+ commit: SparseClassNamePartImpl["commit"],
471
+ values: Array<string | Array<string>>
472
+ ) {
473
+ super(commit, values, ReadonlyArray.getEquivalence(strictEq))
474
+ }
475
+
476
+ static browser(
477
+ parts: ReadonlyArray<ClassNamePart | StaticText>,
478
+ element: HTMLElement | SVGElement,
479
+ ctx: RenderContext,
480
+ values: Array<string | Array<string>> = []
481
+ ) {
482
+ return new SparseClassNamePartImpl(
483
+ parts,
484
+ ({ part, value }) =>
485
+ ctx.queue.add(part, () => {
486
+ return element.setAttribute("class", value.flatMap((s) => isNonEmptyString(s, true)).join(" "))
487
+ }),
488
+ values
489
+ )
490
+ }
491
+ }
492
+
493
+ export class SparseCommentPartImpl extends sparse("sparse/comment") implements SparseCommentPart {
494
+ constructor(
495
+ readonly parts: ReadonlyArray<CommentPart | StaticText>,
496
+ commit: SparseCommentPartImpl["commit"],
497
+ value: SparseCommentPartImpl["value"]
498
+ ) {
499
+ super(commit, value, ReadonlyArray.getEquivalence(strictEq))
500
+ }
501
+
502
+ static browser(comment: Comment, parts: ReadonlyArray<CommentPart | StaticText>, ctx: RenderContext) {
503
+ return new SparseCommentPartImpl(
504
+ parts,
505
+ ({ part, value }) =>
506
+ ctx.queue.add(part, () => comment.nodeValue = value.flatMap((s) => isNonEmptyString(s, false)).join("")),
507
+ []
508
+ )
509
+ }
510
+ }
511
+
512
+ export class StaticTextImpl implements StaticText {
513
+ readonly _tag = "static/text"
514
+
515
+ constructor(readonly value: string) {}
516
+ }
517
+
518
+ function isNonEmptyString(s: string | ReadonlyArray<string> | null | undefined, trim: boolean): Array<string> {
519
+ if (s == null) return []
520
+ if (Array.isArray(s)) return s.flatMap((s) => isNonEmptyString(s, trim))
521
+
522
+ const trimmed = trim ? (s as string).trim() : s
523
+
524
+ if (trimmed.length === 0) return []
525
+
526
+ return [trimmed as string]
527
+ }
@@ -0,0 +1,28 @@
1
+ const PATTERN = /(\s*([^>\s]*))/g
2
+ const QUOTES = new Set(`"'`)
3
+
4
+ export type AttrChunk = {
5
+ readonly length: number
6
+ readonly value: string
7
+ }
8
+
9
+ /**
10
+ * Extract an attribute from a chunk of text.
11
+ */
12
+ export default function readAttribute(text: string, pos: number): AttrChunk | null {
13
+ const quote = text.charAt(pos)
14
+ const pos1 = pos + 1
15
+
16
+ if (QUOTES.has(quote)) {
17
+ const nextQuote = text.indexOf(quote, pos1)
18
+ if (nextQuote === -1) {
19
+ return null
20
+ } else {
21
+ return { length: nextQuote - pos + 1, value: text.substring(pos1, nextQuote) }
22
+ }
23
+ } else {
24
+ PATTERN.lastIndex = pos
25
+ const match = PATTERN.exec(text) || []
26
+ return { length: match[1].length, value: match[2] }
27
+ }
28
+ }