@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
package/src/Html.ts ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+
5
+ import * as Fx from "@typed/fx/Fx"
6
+ import { Sink } from "@typed/fx/Sink"
7
+ import { TypeId } from "@typed/fx/TypeId"
8
+ import type { Rendered } from "@typed/wire"
9
+ import { Effect, Option } from "effect"
10
+ import { join } from "effect/ReadonlyArray"
11
+ import type * as Scope from "effect/Scope"
12
+ import { isDirective } from "./Directive"
13
+ import * as ElementRef from "./ElementRef"
14
+ import type { ServerEntry } from "./Entry"
15
+ import type { HtmlChunk, PartChunk, SparsePartChunk, TextChunk } from "./HtmlChunk"
16
+ import { templateToHtmlChunks } from "./HtmlChunk"
17
+ import { parse } from "./internal/parser"
18
+ import { partNodeToPart } from "./internal/server"
19
+ import { TEXT_START, TYPED_END, TYPED_HOLE, TYPED_START } from "./Meta"
20
+ import type { Placeholder } from "./Placeholder"
21
+ import type { Renderable } from "./Renderable"
22
+ import { RenderContext } from "./RenderContext"
23
+ import { HtmlRenderEvent, isRenderEvent } from "./RenderEvent"
24
+ import type { RenderEvent } from "./RenderEvent"
25
+ import { RenderTemplate } from "./RenderTemplate"
26
+ import { TemplateInstance } from "./TemplateInstance"
27
+
28
+ const toHtml = (r: RenderEvent) => (r as HtmlRenderEvent).html
29
+
30
+ /**
31
+ * @since 1.0.0
32
+ */
33
+ export function renderToHtml<R, E>(
34
+ fx: Fx.Fx<R, E, RenderEvent>
35
+ ): Fx.Fx<Exclude<R, RenderTemplate> | RenderContext, E, string> {
36
+ return Fx.fromFxEffect(
37
+ RenderContext.with((ctx) =>
38
+ fx.pipe(
39
+ Fx.provide(RenderTemplate.layer(renderHtml(ctx))),
40
+ Fx.map(toHtml),
41
+ Fx.startWith(TYPED_START),
42
+ Fx.endWith(TYPED_END)
43
+ )
44
+ )
45
+ )
46
+ }
47
+
48
+ /**
49
+ * @since 1.0.0
50
+ */
51
+ export function renderToHtmlString<R, E>(
52
+ fx: Fx.Fx<R, E, RenderEvent>
53
+ ): Effect.Effect<Exclude<R, RenderTemplate> | RenderContext, E, string> {
54
+ return Effect.map(Fx.toReadonlyArray(renderToHtml(fx)), join(""))
55
+ }
56
+
57
+ function renderHtml(ctx: RenderContext) {
58
+ return <Values extends ReadonlyArray<Renderable<any, any>>, T extends Rendered = Rendered>(
59
+ templateStrings: TemplateStringsArray,
60
+ values: Values,
61
+ providedRef?: ElementRef.ElementRef<T>
62
+ ): Effect.Effect<
63
+ Scope.Scope | Placeholder.Context<readonly [] extends Values ? never : Values[number]>,
64
+ never,
65
+ TemplateInstance<
66
+ Placeholder.Error<Values[number]>,
67
+ T
68
+ >
69
+ > => {
70
+ return Effect.gen(function*(_) {
71
+ const ref = providedRef || (yield* _(ElementRef.make()))
72
+ const entry = getServerEntry(templateStrings, ctx.templateCache)
73
+
74
+ if (values.length === 0) {
75
+ return TemplateInstance(Fx.succeed(HtmlRenderEvent((entry.chunks[0] as TextChunk).value)), ref as any)
76
+ } else {
77
+ return TemplateInstance(
78
+ Fx.filter(
79
+ Fx.mergeBuffer(
80
+ entry.chunks.map((chunk) =>
81
+ renderChunk<
82
+ Placeholder.Context<readonly [] extends Values ? never : Values[number]>,
83
+ Placeholder.Error<Values[number]>
84
+ >(chunk, values)
85
+ )
86
+ ),
87
+ (x) => (x.valueOf() as string).length > 0
88
+ ) as any,
89
+ ref as any
90
+ )
91
+ }
92
+ })
93
+ }
94
+ }
95
+
96
+ function renderChunk<R, E>(
97
+ chunk: HtmlChunk,
98
+ values: ReadonlyArray<Renderable<any, any>>
99
+ ): Fx.Fx<R, E, RenderEvent> {
100
+ if (chunk._tag === "text") {
101
+ return Fx.succeed(HtmlRenderEvent(chunk.value))
102
+ } else if (chunk._tag === "part") {
103
+ return renderPart<R, E>(chunk, values)
104
+ } else {
105
+ return renderSparsePart<R, E>(chunk, values) as Fx.Fx<R, E, RenderEvent>
106
+ }
107
+ }
108
+
109
+ function renderNode<R, E>(renderable: Renderable<any, any>): Fx.Fx<R, E, RenderEvent> {
110
+ switch (typeof renderable) {
111
+ case "string":
112
+ case "number":
113
+ case "boolean":
114
+ case "bigint":
115
+ return Fx.succeed(HtmlRenderEvent(TEXT_START + renderable.toString()))
116
+ case "undefined":
117
+ case "object":
118
+ return renderObject(renderable)
119
+ default:
120
+ return Fx.empty
121
+ }
122
+ }
123
+
124
+ function renderObject<R, E>(renderable: object | null | undefined) {
125
+ if (renderable === null || renderable === undefined) {
126
+ return Fx.succeed(HtmlRenderEvent(TEXT_START))
127
+ } else if (Array.isArray(renderable)) {
128
+ return Fx.mergeBuffer(renderable.map(renderNode)) as any
129
+ } else if (Fx.isFx<R, E, Renderable>(renderable)) {
130
+ return Fx.concatMap(takeOneIfNotRenderEvent(renderable), renderNode as any)
131
+ } else if (Effect.isEffect(renderable)) {
132
+ return Fx.switchMap(Fx.fromEffect(renderable as Effect.Effect<R, E, Renderable>), renderNode<R, E>)
133
+ } else if (isRenderEvent(renderable)) {
134
+ return Fx.succeed(renderable)
135
+ } else {
136
+ return Fx.empty
137
+ }
138
+ }
139
+
140
+ function renderPart<R, E>(
141
+ chunk: PartChunk,
142
+ values: ReadonlyArray<Renderable<any, any>>
143
+ ): Fx.Fx<R, E, RenderEvent> {
144
+ const { node, render } = chunk
145
+ const renderable: Renderable<any, any> = values[node.index]
146
+
147
+ // Refs and events are not rendered into HTML
148
+ if (isDirective<R, E>(renderable)) {
149
+ return Fx.fromSink((sink: Sink<E, RenderEvent>) => {
150
+ const part = partNodeToPart(
151
+ node,
152
+ (value) => sink.onSuccess(HtmlRenderEvent(render(value)))
153
+ )
154
+
155
+ return Effect.catchAllCause(renderable(part), sink.onFailure)
156
+ })
157
+ } else if (node._tag === "node") {
158
+ return Fx.endWith(renderNode<R, E>(renderable), HtmlRenderEvent(TYPED_HOLE(node.index)))
159
+ } else {
160
+ const html = Fx.filterMap(Fx.take(unwrapRenderable<R, E>(renderable), 1), (value) => {
161
+ const s = render(value)
162
+
163
+ return s ? Option.some(HtmlRenderEvent(s)) : Option.none()
164
+ })
165
+
166
+ if (node._tag === "text-part") {
167
+ return Fx.endWith(Fx.startWith(html, HtmlRenderEvent(TEXT_START)), HtmlRenderEvent(TYPED_HOLE(node.index)))
168
+ }
169
+
170
+ return html
171
+ }
172
+ }
173
+
174
+ function renderSparsePart<R, E>(
175
+ chunk: SparsePartChunk,
176
+ values: ReadonlyArray<Renderable<any, any>>
177
+ ): Fx.Fx<R, E, RenderEvent> {
178
+ const { node, render } = chunk
179
+
180
+ return Fx.map(
181
+ Fx.take(
182
+ Fx.combine(
183
+ node.nodes.map((node) => {
184
+ if (node._tag === "text") return Fx.succeed(node.value)
185
+
186
+ const renderable: Renderable<any, any> = (values as any)[node.index]
187
+
188
+ if (isDirective<R, E>(renderable)) {
189
+ return Fx.fromSink<R, E, unknown>((sink: Sink<E, unknown>) =>
190
+ Effect.catchAllCause(
191
+ renderable(partNodeToPart(node, (value) => sink.onSuccess(value))),
192
+ sink.onFailure
193
+ )
194
+ )
195
+ }
196
+
197
+ return unwrapRenderable<R, E>(renderable)
198
+ })
199
+ ),
200
+ 1
201
+ ),
202
+ (value) => HtmlRenderEvent(render(value))
203
+ )
204
+ }
205
+
206
+ function takeOneIfNotRenderEvent<R, E, A>(fx: Fx.Fx<R, E, A>): Fx.Fx<R, E, A> {
207
+ return Fx.withEarlyExit(({ fork, sink }) =>
208
+ Fx.run(
209
+ fx,
210
+ Sink(
211
+ sink.onFailure,
212
+ (event) => isRenderEvent(event) ? sink.onSuccess(event) : Effect.zipRight(sink.onSuccess(event), sink.earlyExit)
213
+ )
214
+ ).pipe(
215
+ fork,
216
+ Effect.fromFiberEffect
217
+ )
218
+ )
219
+ }
220
+
221
+ function getServerEntry(
222
+ templateStrings: TemplateStringsArray,
223
+ templateCache: RenderContext["templateCache"]
224
+ ): ServerEntry {
225
+ const cached = templateCache.get(templateStrings)
226
+
227
+ if (cached === undefined || cached._tag === "Browser") {
228
+ const template = parse(templateStrings)
229
+
230
+ const entry: ServerEntry = {
231
+ _tag: "Server",
232
+ template,
233
+ chunks: templateToHtmlChunks(template)
234
+ }
235
+
236
+ return entry
237
+ } else {
238
+ return cached
239
+ }
240
+ }
241
+
242
+ function unwrapRenderable<R, E>(renderable: Renderable<any, any>): Fx.Fx<R, E, any> {
243
+ switch (typeof renderable) {
244
+ case "undefined":
245
+ case "object": {
246
+ if (renderable === null || renderable === undefined) return Fx.succeed(null)
247
+ else if (Array.isArray(renderable)) {
248
+ return Fx.combine(renderable.map(unwrapRenderable)) as any
249
+ } else if (TypeId in renderable) {
250
+ return renderable as any
251
+ } else if (Effect.EffectTypeId in renderable) {
252
+ return Fx.fromFxEffect(Effect.map(renderable as any, unwrapRenderable<any, any>))
253
+ } else return Fx.succeed(renderable as any)
254
+ }
255
+ default:
256
+ return Fx.succeed(renderable)
257
+ }
258
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { TYPED_HASH } from "./Meta"
5
+ import type {
6
+ Attribute,
7
+ ElementNode,
8
+ Node,
9
+ PartNode,
10
+ SelfClosingElementNode,
11
+ SparseAttrNode,
12
+ SparseClassNameNode,
13
+ Template,
14
+ Text,
15
+ TextOnlyElement
16
+ } from "./Template"
17
+
18
+ /**
19
+ * @since 1.0.0
20
+ */
21
+ export type HtmlChunk = TextChunk | PartChunk | SparsePartChunk
22
+
23
+ /**
24
+ * @since 1.0.0
25
+ */
26
+ export class TextChunk {
27
+ readonly _tag = "text"
28
+ constructor(readonly value: string) {}
29
+ }
30
+
31
+ /**
32
+ * @since 1.0.0
33
+ */
34
+ export class PartChunk {
35
+ readonly _tag = "part"
36
+
37
+ constructor(
38
+ readonly node: PartNode,
39
+ readonly render: (value: unknown) => string
40
+ ) {}
41
+ }
42
+
43
+ /**
44
+ * @since 1.0.0
45
+ */
46
+ export class SparsePartChunk {
47
+ readonly _tag = "sparse-part"
48
+
49
+ constructor(
50
+ readonly node: SparseAttrNode | SparseClassNameNode,
51
+ readonly render: (value: AttrValue) => string
52
+ ) {}
53
+ }
54
+
55
+ /**
56
+ * @since 1.0.0
57
+ */
58
+ export type AttrValue = string | null | undefined | ReadonlyArray<AttrValue>
59
+
60
+ // TODO: Should we escape more things?
61
+ // TODO: We should manually optimize the text fusion
62
+
63
+ /**
64
+ * @since 1.0.0
65
+ */
66
+ export function templateToHtmlChunks({ hash, nodes }: Template) {
67
+ return fuseTextChunks(nodes.flatMap((node) => nodeToHtmlChunk(node, hash)))
68
+ }
69
+
70
+ function fuseTextChunks(chunks: Array<HtmlChunk>): ReadonlyArray<HtmlChunk> {
71
+ const output: Array<HtmlChunk> = []
72
+
73
+ for (let i = 0; i < chunks.length; i++) {
74
+ if (i > 0) {
75
+ const prevIndex = output.length - 1
76
+ const prev = output[prevIndex]
77
+ const curr = chunks[i]
78
+
79
+ if (prev._tag === "text" && curr._tag === "text") {
80
+ output[prevIndex] = new TextChunk(prev.value + curr.value)
81
+ } else {
82
+ output.push(curr)
83
+ }
84
+ } else {
85
+ output.push(chunks[i])
86
+ }
87
+ }
88
+
89
+ return output
90
+ }
91
+
92
+ type NodeMap = {
93
+ readonly [K in Node["_tag"]]: (node: Extract<Node, { _tag: K }>, hash?: string) => Array<HtmlChunk>
94
+ }
95
+
96
+ const nodeMap: NodeMap = {
97
+ element: elementToHtmlChunks,
98
+ node: (node) => [new PartChunk(node, String)],
99
+ "self-closing-element": selfClosingElementToHtmlChunks,
100
+ text: (node) => [textToHtmlChunks(node)],
101
+ "text-only-element": textOnlyElementToHtmlChunks,
102
+ comment: (node) => [new TextChunk(`<!--${node.value}-->`)],
103
+ "comment-part": (node) => [
104
+ new PartChunk(node, (value) => `<!--${value}-->`)
105
+ ],
106
+ "sparse-comment": (node) => [
107
+ new TextChunk("<!--"),
108
+ ...node.nodes.map((node) => {
109
+ if (node._tag === "text") {
110
+ return textToHtmlChunks(node)
111
+ } else {
112
+ return new PartChunk(node, (value) => `${value}`)
113
+ }
114
+ }),
115
+ new TextChunk("-->")
116
+ ]
117
+ }
118
+
119
+ function nodeToHtmlChunk(node: Node, hash?: string): Array<HtmlChunk> {
120
+ return nodeMap[node._tag](node as any, hash)
121
+ }
122
+
123
+ function elementToHtmlChunks(
124
+ { attributes, children, tagName }: ElementNode,
125
+ hash?: string
126
+ ): Array<HtmlChunk> {
127
+ if (attributes.length === 0) {
128
+ return [
129
+ new TextChunk(openTag(tagName, hash) + ">"),
130
+ ...children.flatMap((c) => nodeToHtmlChunk(c)),
131
+ new TextChunk(closeTag(tagName))
132
+ ]
133
+ }
134
+
135
+ const chunks: Array<HtmlChunk> = [
136
+ new TextChunk(openTag(tagName, hash)),
137
+ ...attributes.map((a) => attributeToHtmlChunk(a)),
138
+ new TextChunk(">"),
139
+ ...children.flatMap((c) => nodeToHtmlChunk(c)),
140
+ new TextChunk(closeTag(tagName))
141
+ ]
142
+
143
+ return chunks
144
+ }
145
+
146
+ function selfClosingElementToHtmlChunks(
147
+ { attributes, tagName }: SelfClosingElementNode,
148
+ hash?: string
149
+ ): Array<HtmlChunk> {
150
+ if (attributes.length === 0) {
151
+ return [new TextChunk(openTag(tagName, hash) + "/>")]
152
+ }
153
+
154
+ const chunks: Array<HtmlChunk> = [
155
+ new TextChunk(openTag(tagName, hash)),
156
+ ...attributes.map((a) => attributeToHtmlChunk(a)),
157
+ new TextChunk(`/>`)
158
+ ]
159
+
160
+ return chunks
161
+ }
162
+
163
+ function textToHtmlChunks(text: Text): HtmlChunk {
164
+ return text._tag === "text" ? new TextChunk(text.value) : new PartChunk(text, String)
165
+ }
166
+
167
+ function textOnlyElementToHtmlChunks(
168
+ { attributes, children, tagName }: TextOnlyElement,
169
+ hash?: string
170
+ ): Array<HtmlChunk> {
171
+ if (attributes.length === 0) {
172
+ return [
173
+ new TextChunk(openTag(tagName, hash) + ">"),
174
+ ...children.map((c) => textToHtmlChunks(c)),
175
+ new TextChunk(closeTag(tagName))
176
+ ]
177
+ }
178
+
179
+ const chunks: Array<HtmlChunk> = [
180
+ new TextChunk(openTag(tagName, hash)),
181
+ ...attributes.map((a) => attributeToHtmlChunk(a)),
182
+ new TextChunk(">"),
183
+ ...children.map((c) => textToHtmlChunks(c)),
184
+ new TextChunk(closeTag(tagName))
185
+ ]
186
+
187
+ return chunks
188
+ }
189
+
190
+ type AttrMap = {
191
+ [K in Attribute["_tag"]]: (attr: Extract<Attribute, { readonly _tag: K }>) => HtmlChunk
192
+ }
193
+
194
+ const attrMap: AttrMap = {
195
+ attribute: (attr) => new TextChunk(` ${attr.name}="${attr.value}"`),
196
+ attr: (attr) => new PartChunk(attr, (value) => (value == null ? `` : ` ${attr.name}="${value}"`)),
197
+ boolean: (attr) => new TextChunk(" " + attr.name),
198
+ "boolean-part": (attr) => new PartChunk(attr, (value) => (value ? ` ${attr.name}` : "")),
199
+ "className-part": (attr) => new PartChunk(attr, (value) => (value ? ` class="${value}"` : "")),
200
+ data: (attr) =>
201
+ new PartChunk(attr, (value) => value == null ? `` : datasetToString(value as Readonly<Record<string, string>>)),
202
+ event: () => new TextChunk(""),
203
+ property: (attr) => new PartChunk(attr, (value) => (value == null ? `` : ` ${attr.name}="${escape(value)}"`)),
204
+ ref: () => new TextChunk(""),
205
+ "sparse-attr": (attr) =>
206
+ new SparsePartChunk(attr, (values) => {
207
+ return values == null
208
+ ? ``
209
+ : ` ${attr.name}="${Array.isArray(values) ? values.filter(isString).join("") : values}"`
210
+ }),
211
+ "sparse-class-name": (attr) =>
212
+ new SparsePartChunk(attr, (values) => {
213
+ return values == null
214
+ ? ``
215
+ : ` class="${Array.isArray(values) ? values.filter(isString).join(" ") : values}"`
216
+ }),
217
+ text: (attr) => new TextChunk(attr.value)
218
+ }
219
+
220
+ function attributeToHtmlChunk(attr: Attribute): HtmlChunk {
221
+ return attrMap[attr._tag](attr as any)
222
+ }
223
+
224
+ function isString(value: unknown): value is string {
225
+ return typeof value === "string"
226
+ }
227
+
228
+ function datasetToString(dataset: Readonly<Record<string, string | undefined>>) {
229
+ const s = Object.entries(dataset)
230
+ .map(([key, value]) => (value === undefined ? `data-${key}` : `data-${key}="${value}"`))
231
+ .join(" ")
232
+
233
+ return s.length === 0 ? "" : " " + s
234
+ }
235
+
236
+ function openTag(tagName: string, hash?: string): string {
237
+ if (hash === undefined) return `<${tagName}`
238
+
239
+ return `<${tagName} ${TYPED_HASH(hash)}`
240
+ }
241
+
242
+ function closeTag(tagName: string): string {
243
+ return `</${tagName}>`
244
+ }
245
+
246
+ /**
247
+ * @since 1.0.0
248
+ */
249
+ export function escape(s: unknown) {
250
+ switch (typeof s) {
251
+ case "string":
252
+ case "number":
253
+ case "boolean":
254
+ return escapeHtml(String(s))
255
+ default:
256
+ return escapeHtml(JSON.stringify(s))
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @since 1.0.0
262
+ */
263
+ export function unescape(s: string) {
264
+ const unescaped = unescapeHtml(s)
265
+ const couldBeJson = unescaped[0] === "[" || unescaped === "{"
266
+ if (couldBeJson) {
267
+ try {
268
+ return JSON.parse(unescaped)
269
+ } catch {
270
+ return unescaped
271
+ }
272
+ } else {
273
+ return unescaped
274
+ }
275
+ }
276
+
277
+ const unescapeHtmlRules = [
278
+ [/&quot;/g, "\""],
279
+ [/&#39;/g, "'"],
280
+ [/&#x3A;/g, ":"],
281
+ [/&lt;/g, "<"],
282
+ [/&gt;/g, ">"],
283
+ [/&amp;/g, "&"]
284
+ ] as const
285
+
286
+ const matchHtmlRegExp = /["'&<>]/
287
+
288
+ /**
289
+ * @since 1.0.0
290
+ */
291
+ export function escapeHtml(str: string): string {
292
+ const match = matchHtmlRegExp.exec(str)
293
+
294
+ if (!match) {
295
+ return str
296
+ }
297
+
298
+ let escape
299
+ let html = ""
300
+ let index = 0
301
+ let lastIndex = 0
302
+
303
+ for (index = match.index; index < str.length; index++) {
304
+ switch (str.charCodeAt(index)) {
305
+ case 34: // "
306
+ escape = "&quot;"
307
+ break
308
+ case 38: // &
309
+ escape = "&amp;"
310
+ break
311
+ case 39: // '
312
+ escape = "&#39;"
313
+ break
314
+ case 60: // <
315
+ escape = "&lt;"
316
+ break
317
+ case 62: // >
318
+ escape = "&gt;"
319
+ break
320
+ default:
321
+ continue
322
+ }
323
+
324
+ if (lastIndex !== index) {
325
+ html += str.substring(lastIndex, index)
326
+ }
327
+
328
+ lastIndex = index + 1
329
+ html += escape
330
+ }
331
+
332
+ return lastIndex !== index
333
+ ? html + str.substring(lastIndex, index)
334
+ : html
335
+ }
336
+
337
+ /**
338
+ * @since 1.0.0
339
+ */
340
+ export function unescapeHtml(html: string) {
341
+ for (const [from, to] of unescapeHtmlRules) {
342
+ html = html.replace(from, to)
343
+ }
344
+
345
+ return html
346
+ }
package/src/Hydrate.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+
5
+ import * as Context from "@typed/context"
6
+ import { Document } from "@typed/dom/Document"
7
+ import { RootElement } from "@typed/dom/RootElement"
8
+ import * as Fx from "@typed/fx/Fx"
9
+ import * as Effect from "effect/Effect"
10
+ import * as Layer from "effect/Layer"
11
+ import { findRootParentChildNodes, hydrateTemplate } from "./internal/hydrate"
12
+ import { HydrateContext } from "./internal/HydrateContext"
13
+ import { attachRoot } from "./internal/render"
14
+ import type { ToRendered } from "./Render"
15
+ import { RenderContext } from "./RenderContext"
16
+ import { type RenderEvent } from "./RenderEvent"
17
+ import { RenderTemplate } from "./RenderTemplate"
18
+
19
+ /**
20
+ * @since 1.0.0
21
+ */
22
+ export function hydrate<R, E, T extends RenderEvent | null>(
23
+ rendered: Fx.Fx<R, E, T>
24
+ ): Fx.Fx<Exclude<R, RenderTemplate> | Document | RenderContext | RootElement, E, ToRendered<T>> {
25
+ return Fx.fromFxEffect(Effect.contextWith((context) => {
26
+ const [document, renderContext, { rootElement }] = Context.getMany(context, Document, RenderContext, RootElement)
27
+ const ctx: HydrateContext = {
28
+ where: findRootParentChildNodes(rootElement),
29
+ rootIndex: -1,
30
+ parentTemplate: null,
31
+ hydrate: true
32
+ }
33
+
34
+ const layer = Layer.provideMerge(
35
+ HydrateContext.layer(ctx),
36
+ RenderTemplate.layer(hydrateTemplate(document, renderContext))
37
+ )
38
+
39
+ return Fx.provide(
40
+ Fx.mapEffect(rendered, (what) => attachRoot(renderContext.renderCache, rootElement, what)),
41
+ layer
42
+ )
43
+ })) as Fx.Fx<Exclude<R, RenderTemplate> | Document | RenderContext | RootElement, E, ToRendered<T>>
44
+ }
45
+
46
+ /**
47
+ * @since 1.0.0
48
+ */
49
+ export function hydrateLayer<R, E, T extends RenderEvent | null>(
50
+ rendered: Fx.Fx<R, E, T>
51
+ ) {
52
+ return Fx.drainLayer(Fx.switchMapCause(hydrate(rendered), Effect.logError))
53
+ }