@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,293 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type { HtmlChunk, PartChunk, SparsePartChunk } from "../HtmlChunk"
3
+ import type {
4
+ AttributePart,
5
+ ClassNamePart,
6
+ CommentPart,
7
+ Part,
8
+ SparseAttributePart,
9
+ SparseClassNamePart,
10
+ SparseCommentPart,
11
+ SparsePart,
12
+ StaticText
13
+ } from "../Part"
14
+ import type { Renderable } from "../Renderable"
15
+ import type { PartNode, SparseAttrNode, SparseClassNameNode, SparseCommentNode, SparsePartNode } from "../Template"
16
+ import {
17
+ AttributePartImpl,
18
+ BooleanPartImpl,
19
+ ClassNamePartImpl,
20
+ CommentPartImpl,
21
+ DataPartImpl,
22
+ NodePartImpl,
23
+ PropertyPartImpl,
24
+ SparseAttributePartImpl,
25
+ SparseClassNamePartImpl,
26
+ SparseCommentPartImpl,
27
+ StaticTextImpl,
28
+ TextPartImpl
29
+ } from "./parts"
30
+
31
+ export type RenderChunk<R, E> =
32
+ | TextRenderChunk
33
+ | PartRenderChunk<R, E>
34
+ | SparsePartRenderChunk<R, E>
35
+
36
+ export class TextRenderChunk {
37
+ readonly type = "text"
38
+
39
+ constructor(
40
+ readonly index: number,
41
+ readonly value: string
42
+ ) {}
43
+ }
44
+
45
+ export class PartRenderChunk<R, E> {
46
+ readonly type = "part"
47
+
48
+ constructor(
49
+ readonly index: number,
50
+ readonly chunk: PartChunk,
51
+ readonly part: Part,
52
+ readonly renderable: Renderable<R, E>
53
+ ) {}
54
+ }
55
+
56
+ export class SparsePartRenderChunk<R, E> {
57
+ readonly type = "sparse-part"
58
+
59
+ constructor(
60
+ readonly index: number,
61
+ readonly chunk: SparsePartChunk,
62
+ readonly part: SparsePart,
63
+ readonly renderables: Array<Renderable<R, E>>
64
+ ) {}
65
+ }
66
+
67
+ type RenderChunkMap = {
68
+ readonly [K in HtmlChunk["_tag"]]: <R, E>(
69
+ chunk: Extract<HtmlChunk, { _tag: K }>,
70
+ index: number,
71
+ values: ReadonlyArray<Renderable<any, any>>,
72
+ onChunk: (index: number, value: string) => Effect.Effect<never, never, void>
73
+ ) => RenderChunk<R, E>
74
+ }
75
+
76
+ const renderChunkMap: RenderChunkMap = {
77
+ text: (chunk, index) => new TextRenderChunk(index, chunk.value),
78
+ part: (chunk, index, values, onChunk) =>
79
+ // @ts-expect-error
80
+ new PartRenderChunk(
81
+ index,
82
+ chunk,
83
+ partNodeToPart(chunk.node, (v) => onChunk(index, chunk.render(v))),
84
+ values[chunk.node.index]
85
+ ),
86
+ "sparse-part": (chunk, index, values, onChunk) =>
87
+ // @ts-expect-error
88
+ new SparsePartRenderChunk(
89
+ index,
90
+ chunk,
91
+ sparsePartNodeToPart(chunk.node, (v) => onChunk(index, chunk.render(v))),
92
+ // @ts-expect-error
93
+ chunk.node.nodes.map((n) => (n._tag === "text" ? n.value : values[n.index]))
94
+ )
95
+ }
96
+
97
+ export function htmlChunksToRenderChunks<R, E>(
98
+ chunks: ReadonlyArray<HtmlChunk>,
99
+ values: ReadonlyArray<Renderable<R, E>>,
100
+ onChunk: (index: number, value: string) => Effect.Effect<never, never, void>
101
+ ) {
102
+ const output: Array<RenderChunk<R, E>> = Array(chunks.length)
103
+
104
+ for (let i = 0; i < chunks.length; i++) {
105
+ // @ts-expect-error
106
+ output[i] = renderChunkMap[chunks[i]._tag](chunks[i] as any, i, values, onChunk)
107
+ }
108
+
109
+ return output
110
+ }
111
+
112
+ type PartNodeMap = {
113
+ readonly [K in PartNode["_tag"]]: (
114
+ node: Extract<PartNode, { _tag: K }>,
115
+ onChunk: (value: unknown) => Effect.Effect<never, never, void>
116
+ ) => Part
117
+ }
118
+
119
+ const partNodeMap: PartNodeMap = {
120
+ attr: (node, onChunk) => new AttributePartImpl(node.name, node.index, ({ value }) => onChunk(value), null),
121
+ "boolean-part": (node, onChunk) => new BooleanPartImpl(node.name, node.index, ({ value }) => onChunk(value), null),
122
+ "className-part": (node, onChunk) => new ClassNamePartImpl(node.index, ({ value }) => onChunk(value), null),
123
+ "comment-part": (node, onChunk) => new CommentPartImpl(node.index, ({ value }) => onChunk(value), null),
124
+ "data": (node, onChunk) => new DataPartImpl(node.index, ({ value }) => onChunk(value), null),
125
+ "event": () => {
126
+ throw new Error("Events are not utilized on the server")
127
+ },
128
+ "node": (node, onChunk) => new NodePartImpl(node.index, ({ value }) => onChunk(value), null),
129
+ "property": (node, onChunk) => new PropertyPartImpl(node.name, node.index, ({ value }) => onChunk(value), null),
130
+ "ref": () => {
131
+ throw new Error("Refs are not utilized on the server")
132
+ },
133
+ "text-part": (node, onChunk) => new TextPartImpl(node.index, ({ value }) => onChunk(value), null)
134
+ }
135
+
136
+ export function partNodeToPart(
137
+ node: PartNode,
138
+ onChunk: (value: unknown) => Effect.Effect<never, never, void>
139
+ ): Part {
140
+ return partNodeMap[node._tag](node as any, onChunk)
141
+ }
142
+
143
+ function sparsePartNodeToPart(
144
+ node: SparsePartNode,
145
+ onChunk: (value: string | null) => Effect.Effect<never, never, void>
146
+ ) {
147
+ if (node._tag === "sparse-attr") {
148
+ return renderSparseAttr(node, onChunk)
149
+ } else if (node._tag === "sparse-class-name") {
150
+ return renderSparseClassName(node, onChunk)
151
+ } else {
152
+ return renderSparseComment(node, onChunk)
153
+ }
154
+ }
155
+
156
+ function renderSparseAttr(
157
+ attrNode: SparseAttrNode,
158
+ setAttribute: (value: string | null) => Effect.Effect<never, never, void>
159
+ ): SparseAttributePart {
160
+ const { nodes } = attrNode
161
+ const values: Map<number, string | null> = new Map()
162
+
163
+ function getValue() {
164
+ return (part.value = Array.from({ length: nodes.length }, (_, i) => values.get(i) || ""))
165
+ }
166
+
167
+ function setValue(value: string | null, index: number) {
168
+ return Effect.suspend(() => {
169
+ values.set(index, value)
170
+
171
+ if (values.size === nodes.length) return setAttribute(getValue().join(""))
172
+
173
+ return Effect.unit
174
+ })
175
+ }
176
+
177
+ const parts: Array<StaticText | AttributePart> = []
178
+
179
+ let partIndex = 0
180
+ for (let i = 0; i < nodes.length; i++) {
181
+ const node = nodes[i]
182
+
183
+ if (node._tag === "text") {
184
+ values.set(i, node.value)
185
+ parts.push(new StaticTextImpl(node.value))
186
+ } else {
187
+ parts.push(
188
+ new AttributePartImpl(
189
+ node.name,
190
+ partIndex++,
191
+ ({ value }) => setValue(value || "", i),
192
+ null
193
+ )
194
+ )
195
+ }
196
+ }
197
+
198
+ const part = new SparseAttributePartImpl(attrNode.name, parts, ({ value }) => setAttribute(value.join("")))
199
+
200
+ return part
201
+ }
202
+
203
+ function renderSparseClassName(
204
+ classNameNode: SparseClassNameNode,
205
+ setClassName: (value: string | null) => Effect.Effect<never, never, void>
206
+ ): SparseClassNamePart {
207
+ const { nodes } = classNameNode
208
+ const values: Map<number, string | null> = new Map()
209
+
210
+ function getValue() {
211
+ return (part.value = Array.from({ length: nodes.length }, (_, i) => values.get(i) || ""))
212
+ }
213
+
214
+ function setValue(value: string | null, index: number) {
215
+ return Effect.suspend(() => {
216
+ values.set(index, value)
217
+
218
+ if (values.size === nodes.length) return setClassName(getValue().join(""))
219
+
220
+ return Effect.unit
221
+ })
222
+ }
223
+
224
+ const parts: Array<StaticText | ClassNamePart> = []
225
+
226
+ let partIndex = 0
227
+ for (let i = 0; i < nodes.length; i++) {
228
+ const node = nodes[i]
229
+
230
+ if (node._tag === "text") {
231
+ values.set(i, node.value)
232
+ parts.push(new StaticTextImpl(node.value))
233
+ } else {
234
+ parts.push(
235
+ new ClassNamePartImpl(
236
+ partIndex++,
237
+ ({ value }) => setValue(value?.join(" ") || null, i),
238
+ null
239
+ )
240
+ )
241
+ }
242
+ }
243
+
244
+ const part = new SparseClassNamePartImpl(parts, ({ value }) => setClassName(value.join("")), [])
245
+
246
+ return part
247
+ }
248
+
249
+ function renderSparseComment(
250
+ commentNode: SparseCommentNode,
251
+ setComment: (value: string | null) => Effect.Effect<never, never, void>
252
+ ): SparseCommentPart {
253
+ const { nodes } = commentNode
254
+ const values: Map<number, string | null | undefined> = new Map()
255
+
256
+ function getValue(): ReadonlyArray<string> {
257
+ return (part.value = Array.from({ length: nodes.length }, (_, i) => values.get(i) || ""))
258
+ }
259
+
260
+ function setValue(value: string | null | undefined, index: number) {
261
+ return Effect.suspend(() => {
262
+ values.set(index, value)
263
+
264
+ if (values.size === nodes.length) return setComment(getValue().join(""))
265
+
266
+ return Effect.unit
267
+ })
268
+ }
269
+
270
+ const parts: Array<StaticText | CommentPart> = []
271
+
272
+ let partIndex = 0
273
+ for (let i = 0; i < nodes.length; i++) {
274
+ const node = nodes[i]
275
+
276
+ if (node._tag === "text") {
277
+ values.set(i, node.value)
278
+ parts.push(new StaticTextImpl(node.value))
279
+ } else {
280
+ parts.push(
281
+ new CommentPartImpl(
282
+ partIndex++,
283
+ ({ value }) => setValue(value, i),
284
+ null
285
+ )
286
+ )
287
+ }
288
+ }
289
+
290
+ const part = new SparseCommentPartImpl(parts, ({ value }) => setComment(value.join("")), [])
291
+
292
+ return part
293
+ }
@@ -0,0 +1,338 @@
1
+ import * as Token from "../Token"
2
+ import {
3
+ getAllTextUntilElementClose,
4
+ getAttributeWithoutQuotes,
5
+ getAttributeWithQuotes,
6
+ getBooleanAttribute,
7
+ getClosingTag,
8
+ getComment,
9
+ getOpeningTag,
10
+ getOpeningTagEnd,
11
+ getPart,
12
+ getSelfClosingTagEnd,
13
+ getTextUntilCloseBrace,
14
+ getTextUntilPart,
15
+ getWhitespace,
16
+ PART_REGEX,
17
+ PART_STRING
18
+ } from "./chunks"
19
+ import type { TextChunk } from "./chunks"
20
+
21
+ export function tokenize(template: ReadonlyArray<string>): Iterable<Token.Token> {
22
+ return new Tokenizer(template)
23
+ }
24
+
25
+ function templateWithParts(template: ReadonlyArray<string>): string {
26
+ const length = template.length
27
+ const lastIndex = length - 1
28
+
29
+ let output = ""
30
+
31
+ for (let i = 0; i < length; i++) {
32
+ const str = template[i]
33
+
34
+ if (i === lastIndex) {
35
+ output += str
36
+ } else {
37
+ output += str + PART_STRING(i)
38
+ }
39
+ }
40
+
41
+ return output
42
+ }
43
+
44
+ class Tokenizer implements Iterable<Token.Token> {
45
+ protected input: string = ""
46
+ protected pos: number = 0
47
+ protected context: "text" | "element" | "self-closing" | "text-only" = "text"
48
+ protected currentTag: Stack<string> | null = null
49
+
50
+ constructor(private readonly template: ReadonlyArray<string>) {}
51
+
52
+ *[Symbol.iterator](): Generator<Token.Token> {
53
+ this.init()
54
+
55
+ // eslint-disable-next-line no-constant-condition
56
+ while (this.pos < this.input.length) {
57
+ switch (this.context) {
58
+ case "element":
59
+ yield* this.nextElementToken()
60
+ break
61
+ case "self-closing":
62
+ yield* this.nextSelfClosingToken()
63
+ break
64
+ case "text-only":
65
+ yield* this.nextTextOnlyToken()
66
+ break
67
+ default:
68
+ yield* this.nextTextToken()
69
+ }
70
+ }
71
+ }
72
+
73
+ private init(): void {
74
+ // Convert our template into a string for easier tokenization
75
+ // Remove any whitespace at the beginning or end of the template since we don't care about it.
76
+ this.input = this.template.length === 1 ? this.template[0] : templateWithParts(this.template)
77
+ }
78
+
79
+ private *nextTextToken(): Generator<Token.Token> {
80
+ const char = this.nextChar()
81
+ const isOpenBracket = char === "<"
82
+
83
+ let next: TextChunk | undefined
84
+
85
+ if (isOpenBracket && (next = this.chunk(getOpeningTag))) {
86
+ const name = next.match[2]
87
+
88
+ this.pushTag(name)
89
+ this.context = Token.SELF_CLOSING_TAGS.has(name)
90
+ ? "self-closing"
91
+ : Token.TEXT_ONLY_NODES_REGEX.has(name)
92
+ ? "text-only"
93
+ : "element"
94
+
95
+ yield new Token.OpeningTagToken(name)
96
+ } else if (isOpenBracket && (next = this.chunk(getClosingTag))) {
97
+ yield new Token.ClosingTagToken(next.match[3])
98
+ this.popTag()
99
+ } else if (isOpenBracket && (next = this.chunk(getComment))) {
100
+ yield* this.parseComment(next.match[2])
101
+ } else if ((next = this.chunk(getPart))) {
102
+ yield new Token.PartToken(parseInt(next.match[2], 10))
103
+ } else if ((next = this.chunk(getWhitespace))) {
104
+ yield new Token.TextToken(next.match[1])
105
+ } else if ((next = this.chunk(getTextUntilCloseBrace))) {
106
+ yield* parseTextAndParts(next.match[1])
107
+ } else {
108
+ yield new Token.TextToken(this.takeNextChar())
109
+ }
110
+ }
111
+
112
+ private *nextElementToken(): Generator<Token.Token> {
113
+ let next: TextChunk | undefined
114
+
115
+ if ((next = this.chunk(getAttributeWithQuotes))) {
116
+ yield* this.parseAttribute(next.match[2], next.match[4])
117
+ } else if ((next = this.chunk(getAttributeWithoutQuotes))) {
118
+ yield* this.parseAttribute(next.match[2], next.match[4])
119
+ } else if ((next = this.chunk(getOpeningTagEnd))) {
120
+ this.context = "text"
121
+
122
+ yield new Token.OpeningTagEndToken(this.currentTag!.value)
123
+ } else if ((next = this.chunk(getSelfClosingTagEnd))) {
124
+ yield new Token.OpeningTagEndToken(this.currentTag!.value, false)
125
+ yield new Token.ClosingTagToken(this.currentTag!.value)
126
+ this.popTag()
127
+ this.context = "text"
128
+ } else if ((next = this.chunk(getBooleanAttribute))) {
129
+ yield new Token.BooleanAttributeToken(next.match[2])
130
+ } else if ((next = this.chunk(getWhitespace))) {
131
+ // Continue
132
+ } else {
133
+ this.invalidTemplate()
134
+ }
135
+ }
136
+
137
+ private *nextSelfClosingToken(): Generator<Token.Token> {
138
+ let next: TextChunk | undefined
139
+
140
+ if ((next = this.chunk(getAttributeWithQuotes))) {
141
+ yield* this.parseAttribute(next.match[2], next.match[4])
142
+ } else if ((next = this.chunk(getAttributeWithoutQuotes))) {
143
+ yield* this.parseAttribute(next.match[2], next.match[4])
144
+ } else if ((next = this.chunk(getSelfClosingTagEnd))) {
145
+ yield new Token.OpeningTagEndToken(this.currentTag!.value, true)
146
+ this.popTag()
147
+ this.context = "text"
148
+ } else if ((next = this.chunk(getBooleanAttribute))) {
149
+ yield new Token.BooleanAttributeToken(next.match[2])
150
+ } else {
151
+ this.invalidTemplate()
152
+ }
153
+ }
154
+
155
+ private *nextTextOnlyToken(): Generator<Token.Token> {
156
+ let next: TextChunk | undefined
157
+
158
+ if ((next = this.chunk(getAttributeWithQuotes))) {
159
+ yield* this.parseAttribute(next.match[2], next.match[4])
160
+ } else if ((next = this.chunk(getAttributeWithoutQuotes))) {
161
+ yield* this.parseAttribute(next.match[2], next.match[4])
162
+ } else if ((next = this.chunk(getOpeningTagEnd))) {
163
+ yield new Token.OpeningTagEndToken(this.currentTag!.value)
164
+ } else if ((next = this.chunk(getClosingTag))) {
165
+ yield new Token.ClosingTagToken(next.match[3])
166
+ this.context = "text"
167
+ } else if ((next = this.chunk(getAllTextUntilElementClose(this.currentTag!.value)))) {
168
+ const text = next.match[1]
169
+
170
+ if (PART_REGEX.test(text)) {
171
+ yield* parseTextAndParts(text)
172
+ } else {
173
+ yield new Token.TextToken(text)
174
+ }
175
+ this.context = "text"
176
+ } else {
177
+ this.invalidTemplate()
178
+ }
179
+ }
180
+
181
+ private *parseComment(comment: string): Generator<Token.Token> {
182
+ const parts = Array.from(parseTextAndParts(comment))
183
+
184
+ if (parts.length === 1 && parts[0]._tag === "text") {
185
+ yield new Token.CommentToken(parts[0].value)
186
+ return
187
+ }
188
+
189
+ yield new Token.CommentStartToken("<!--")
190
+
191
+ for (const part of parts) {
192
+ yield part
193
+ }
194
+
195
+ yield new Token.CommentEndToken("-->")
196
+ }
197
+
198
+ private *parseAttribute(name: string, value: string) {
199
+ const parts = Array.from(parseTextAndParts(value))
200
+
201
+ if (value === "") {
202
+ yield new Token.BooleanAttributeToken(name)
203
+ return
204
+ }
205
+
206
+ if (parts.length === 1 && parts[0]._tag === "text") {
207
+ yield getAttributeToken(name, value)
208
+ return
209
+ }
210
+
211
+ yield getAttributeTokenPartial(name, "start")
212
+
213
+ for (const part of parts) {
214
+ yield part
215
+ }
216
+
217
+ yield getAttributeTokenPartial(name, "end")
218
+ }
219
+
220
+ private nextChar(): string {
221
+ return this.input.charAt(this.pos)
222
+ }
223
+
224
+ private takeNextChar(): string {
225
+ const text = this.input.substring(this.pos, this.pos + 1)
226
+ this.pos += 1
227
+
228
+ return text
229
+ }
230
+
231
+ private chunk(f: (str: string, pos: number) => TextChunk | undefined): TextChunk | undefined {
232
+ const chunk = f(this.input, this.pos)
233
+
234
+ if (chunk) {
235
+ this.pos += chunk.length
236
+ }
237
+
238
+ return chunk
239
+ }
240
+
241
+ private pushTag(name: string): void {
242
+ this.currentTag = new Stack(name, this.currentTag)
243
+ }
244
+
245
+ private popTag(): void {
246
+ if (this.currentTag) {
247
+ this.currentTag = this.currentTag.previous
248
+ }
249
+ }
250
+
251
+ protected invalidTemplate() {
252
+ let message = `Invalid template ${this.input}`
253
+ message += "\n\nFrom:\n\n"
254
+ message += this.input.substring(this.pos)
255
+
256
+ throw new SyntaxError(message)
257
+ }
258
+ }
259
+
260
+ class Stack<A> {
261
+ constructor(
262
+ readonly value: A,
263
+ readonly previous: Stack<A> | null
264
+ ) {}
265
+ }
266
+
267
+ function* parseTextAndParts(s: string): Generator<Token.TextToken | Token.PartToken> {
268
+ let next: TextChunk | undefined
269
+ let pos: number = 0
270
+
271
+ while (pos < s.length) {
272
+ if ((next = getPart(s, pos))) {
273
+ yield new Token.PartToken(parseInt(next.match[2], 10))
274
+ pos += next.length
275
+ } else if ((next = getTextUntilPart(s, pos))) {
276
+ yield new Token.TextToken(next.match[1])
277
+
278
+ pos += next.length
279
+ } else {
280
+ yield new Token.TextToken(s.substring(pos))
281
+
282
+ break
283
+ }
284
+ }
285
+ }
286
+
287
+ function getAttributeTokenPartial(name: string, ctx: "start" | "end") {
288
+ switch (name[0]) {
289
+ case "?":
290
+ return ctx === "start"
291
+ ? new Token.BooleanAttributeStartToken(name.substring(1))
292
+ : new Token.BooleanAttributeEndToken(name.substring(1))
293
+ case ".": {
294
+ const propertyName = name.substring(1)
295
+
296
+ switch (propertyName) {
297
+ case "data":
298
+ return ctx === "start"
299
+ ? new Token.DataAttributeStartToken()
300
+ : new Token.DataAttributeEndToken()
301
+ default:
302
+ return ctx === "start"
303
+ ? new Token.PropertyAttributeStartToken(propertyName)
304
+ : new Token.PropertyAttributeEndToken(propertyName)
305
+ }
306
+ }
307
+ case "@":
308
+ return ctx === "start"
309
+ ? new Token.EventAttributeStartToken(name.substring(1))
310
+ : new Token.EventAttributeEndToken(name.substring(1))
311
+ case "o":
312
+ if (name[1] === "n") {
313
+ return ctx === "start"
314
+ ? new Token.EventAttributeStartToken(name.substring(2))
315
+ : new Token.EventAttributeEndToken(name.substring(2))
316
+ }
317
+ }
318
+
319
+ switch (name.toLowerCase()) {
320
+ case "class":
321
+ case "classname":
322
+ return ctx === "start"
323
+ ? new Token.ClassNameAttributeStartToken()
324
+ : new Token.ClassNameAttributeEndToken()
325
+ case "ref":
326
+ return ctx === "start" ? new Token.RefAttributeStartToken() : new Token.RefAttributeEndToken()
327
+ }
328
+
329
+ return ctx === "start" ? new Token.AttributeStartToken(name) : new Token.AttributeEndToken(name)
330
+ }
331
+
332
+ function getAttributeToken(name: string, value: string) {
333
+ if (name[0] === "?") {
334
+ return new Token.BooleanAttributeToken(name.substring(1))
335
+ }
336
+
337
+ return new Token.AttributeToken(name, value)
338
+ }
@@ -0,0 +1,73 @@
1
+ import { getOption } from "@typed/context"
2
+ import * as Chunk from "effect/Chunk"
3
+ import type * as Duration from "effect/Duration"
4
+ import * as Effect from "effect/Effect"
5
+ import { isSome } from "effect/Option"
6
+ import { TestClock } from "effect/TestClock"
7
+ import { TEXT_START, TYPED_START } from "../Meta"
8
+
9
+ export function isComment(node: Node): node is Comment {
10
+ return node.nodeType === node.COMMENT_NODE
11
+ }
12
+
13
+ export function isCommentWithValue(node: Node, value: string): node is Comment {
14
+ return isComment(node) && node.nodeValue === value
15
+ }
16
+
17
+ export function isHtmlElement(node: Node): node is HTMLElement {
18
+ return node.nodeType === node.ELEMENT_NODE
19
+ }
20
+
21
+ export function findHoleComment(parent: Element, index: number) {
22
+ const childNodes = parent.childNodes
23
+
24
+ for (let i = 0; i < childNodes.length; ++i) {
25
+ const node = childNodes[i]
26
+
27
+ if (node.nodeType === 8 && node.nodeValue === `hole${index}`) {
28
+ return node as Comment
29
+ }
30
+ }
31
+
32
+ throw new Error(`Unable to find hole comment for index ${index}`)
33
+ }
34
+
35
+ const previousComments = new Set([TEXT_START, TYPED_START])
36
+
37
+ export function getPreviousNodes(comment: Node, index: number) {
38
+ const nodes: Array<Node> = []
39
+ let node = comment.previousSibling
40
+ const previousHole = `hole${index - 1}`
41
+
42
+ previousComments.add(previousHole)
43
+
44
+ while (node && !previousComments.has(String(node.nodeValue))) {
45
+ nodes.unshift(node)
46
+ node = node.previousSibling
47
+ }
48
+
49
+ previousComments.delete(previousHole)
50
+
51
+ return nodes
52
+ }
53
+
54
+ export const findPath = (node: ParentChildNodes, path: Chunk.Chunk<number>): Node =>
55
+ Chunk.reduce(path, node, ({ childNodes }, index) => childNodes[index]) as Node
56
+
57
+ export interface ParentChildNodes {
58
+ readonly parentNode: Node | null
59
+ readonly childNodes: ArrayLike<Node>
60
+ }
61
+
62
+ export function adjustTime(input?: Duration.DurationInput) {
63
+ return Effect.gen(function*(_) {
64
+ const ctx = yield* _(Effect.context<never>())
65
+ const testClock = getOption(ctx, TestClock)
66
+
67
+ if (isSome(testClock)) {
68
+ yield* _(testClock.value.adjust(input ?? 1))
69
+ } else if (input) {
70
+ yield* _(Effect.sleep(input))
71
+ }
72
+ })
73
+ }