@typed/template 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/cjs/internal/chunks.js +4 -1
  2. package/dist/cjs/internal/chunks.js.map +1 -1
  3. package/dist/cjs/internal/errors.js +4 -0
  4. package/dist/cjs/internal/errors.js.map +1 -1
  5. package/dist/cjs/internal/hydrate.js +92 -58
  6. package/dist/cjs/internal/hydrate.js.map +1 -1
  7. package/dist/cjs/internal/parser.js +14 -6
  8. package/dist/cjs/internal/parser.js.map +1 -1
  9. package/dist/cjs/internal/render.js +17 -151
  10. package/dist/cjs/internal/render.js.map +1 -1
  11. package/dist/dts/internal/chunks.d.ts +1 -0
  12. package/dist/dts/internal/chunks.d.ts.map +1 -1
  13. package/dist/dts/internal/errors.d.ts +1 -0
  14. package/dist/dts/internal/errors.d.ts.map +1 -1
  15. package/dist/dts/internal/hydrate.d.ts +9 -16
  16. package/dist/dts/internal/hydrate.d.ts.map +1 -1
  17. package/dist/dts/internal/parser.d.ts.map +1 -1
  18. package/dist/dts/internal/render.d.ts +0 -14
  19. package/dist/dts/internal/render.d.ts.map +1 -1
  20. package/dist/esm/internal/chunks.js +2 -0
  21. package/dist/esm/internal/chunks.js.map +1 -1
  22. package/dist/esm/internal/errors.js +3 -0
  23. package/dist/esm/internal/errors.js.map +1 -1
  24. package/dist/esm/internal/hydrate.js +94 -56
  25. package/dist/esm/internal/hydrate.js.map +1 -1
  26. package/dist/esm/internal/parser.js +16 -7
  27. package/dist/esm/internal/parser.js.map +1 -1
  28. package/dist/esm/internal/render.js +18 -147
  29. package/dist/esm/internal/render.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/internal/chunks.ts +4 -0
  32. package/src/internal/errors.ts +4 -0
  33. package/src/internal/hydrate.ts +124 -77
  34. package/src/internal/parser.ts +17 -8
  35. package/src/internal/render.ts +30 -293
@@ -1,5 +1,4 @@
1
1
  import * as Fx from "@typed/fx/Fx"
2
- import type { Cause } from "effect"
3
2
  import * as Effect from "effect/Effect"
4
3
  import type { Placeholder } from "../Placeholder.js"
5
4
  import type { Renderable } from "../Renderable.js"
@@ -7,16 +6,18 @@ import type { RenderContext } from "../RenderContext.js"
7
6
  import type { RenderEvent } from "../RenderEvent.js"
8
7
  import { DomRenderEvent } from "../RenderEvent.js"
9
8
  import type { RenderTemplate } from "../RenderTemplate.js"
10
- import { indexRefCounter } from "./indexRefCounter.js"
9
+ import { indexRefCounter2 } from "./indexRefCounter.js"
11
10
 
12
11
  import { unsafeGet } from "@typed/context"
13
12
 
13
+ import { Either } from "effect"
14
14
  import { Scope } from "effect/Scope"
15
15
  import type { Template } from "../Template.js"
16
16
  import { CouldNotFindCommentError, CouldNotFindRootElement } from "./errors.js"
17
- import { type EventSource, makeEventSource } from "./EventSource.js"
17
+ import { makeEventSource } from "./EventSource.js"
18
18
  import { HydrateContext } from "./HydrateContext.js"
19
- import { buildParts, getBrowserEntry, renderTemplate, renderValues } from "./render.js"
19
+ import type { RenderPartContext } from "./render.js"
20
+ import { getBrowserEntry, renderPart2, renderTemplate } from "./render.js"
20
21
  import {
21
22
  findPath,
22
23
  getPreviousNodes,
@@ -26,8 +27,6 @@ import {
26
27
  type ParentChildNodes
27
28
  } from "./utils.js"
28
29
 
29
- // TODO: Handle missing comment errors
30
-
31
30
  /**
32
31
  * Here for "standard" browser rendering, a TemplateInstance is effectively a live
33
32
  * view into the contents rendered by the Template.
@@ -48,7 +47,6 @@ export const hydrateTemplate: (document: Document, ctx: RenderContext) => Render
48
47
  > => {
49
48
  return Fx.make((sink) =>
50
49
  Effect.gen(function*(_) {
51
- const eventSource = makeEventSource()
52
50
  const context = yield* _(Effect.context<Scope>())
53
51
  const hydrateCtx = unsafeGet(context, HydrateContext)
54
52
  const scope = unsafeGet(context, Scope)
@@ -58,38 +56,71 @@ export const hydrateTemplate: (document: Document, ctx: RenderContext) => Render
58
56
  return render_(templateStrings, values)
59
57
  }
60
58
 
61
- const { parts, template, where, wire } = getHydrateEntry({
59
+ const either = getHydrateEntry({
62
60
  ...hydrateCtx,
63
61
  document,
64
62
  renderContext,
65
- eventSource,
66
- strings: templateStrings,
67
- onCause: sink.onFailure
63
+ strings: templateStrings
68
64
  })
69
65
 
70
- // If there are parts we need to render them before constructing our Wire
71
- if (parts.length > 0) {
72
- const refCounter = yield* _(indexRefCounter(parts.length))
73
-
74
- // Do the work
75
- yield* _(
76
- renderValues(values, parts, refCounter, context, (index: number): HydrateContext => ({
77
- where,
78
- rootIndex: index,
79
- parentTemplate: template,
80
- hydrate: true
81
- }))
82
- )
83
-
84
- // Wait for initial work to be completed
85
- yield* _(refCounter.wait)
66
+ if (Either.isLeft(either)) {
67
+ hydrateCtx.hydrate = false
68
+ return render_(templateStrings, values)
86
69
  }
87
70
 
88
- hydrateCtx.hydrate = false
71
+ const { template, where, wire } = either.right
89
72
 
90
- yield* _(eventSource.setup(wire, scope))
73
+ if (values.length === 0) return yield* _(sink.onSuccess(DomRenderEvent(wire)), Effect.zipRight(Effect.never))
74
+
75
+ const makeHydrateContext = (index: number): HydrateContext => ({
76
+ where,
77
+ rootIndex: index,
78
+ parentTemplate: template,
79
+ hydrate: true
80
+ })
81
+
82
+ const refCounter = yield* _(indexRefCounter2())
83
+ const ctx: RenderPartContext = {
84
+ context,
85
+ document,
86
+ eventSource: makeEventSource(),
87
+ expected: 0,
88
+ refCounter,
89
+ renderContext,
90
+ onCause: sink.onFailure,
91
+ values,
92
+ makeHydrateContext
93
+ }
91
94
 
95
+ // Connect our interpolated values to our template parts
96
+ const effects: Array<Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>> = []
97
+ for (const [part, path] of template.parts) {
98
+ const eff = renderPart2(part, where, path, ctx)
99
+ if (eff !== null) {
100
+ effects.push(
101
+ ...(Array.isArray(eff) ? eff : [eff]) as Array<
102
+ Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>
103
+ >
104
+ )
105
+ }
106
+ }
107
+
108
+ // Fork any effects necessary
109
+ if (effects.length > 0) {
110
+ yield* _(Effect.forkAll(effects))
111
+ }
112
+
113
+ // Set the element when it is ready
114
+ yield* _(ctx.eventSource.setup(wire, scope))
115
+
116
+ // Emit our DomRenderEvent
92
117
  yield* _(sink.onSuccess(DomRenderEvent(wire)))
118
+
119
+ // Stop hydrating
120
+ hydrateCtx.hydrate = false
121
+
122
+ // Ensure our templates last forever in the DOM environment
123
+ // so event listeners are kept attached to the current Scope.
93
124
  yield* _(Effect.never)
94
125
  })
95
126
  )
@@ -110,65 +141,76 @@ const END = "typed-end"
110
141
 
111
142
  // Finds all of the childNodes between the "typed-start" and "typed-end" comments
112
143
  export function findRootChildNodes(where: HTMLElement): Array<Node> {
113
- const childNodes: Array<Node> = []
144
+ let start = -1
145
+ let end = -1
114
146
 
115
- let start = 0
116
- let end = childNodes.length
147
+ const { childNodes } = where
148
+ const length = childNodes.length
117
149
 
118
- for (let i = 0; i < where.childNodes.length; i++) {
119
- const node = where.childNodes[i]
150
+ for (let i = 0; i < length; i++) {
151
+ const node = childNodes[i]
120
152
 
121
- if (node.nodeType === node.COMMENT_NODE) {
122
- if (node.nodeValue === START) {
123
- start = i
124
- break
125
- }
153
+ if (node.nodeType === node.COMMENT_NODE && node.nodeValue === START) {
154
+ start = i
155
+ break
126
156
  }
127
157
  }
128
158
 
129
- for (let i = where.childNodes.length - 1; i >= start; i--) {
130
- const node = where.childNodes[i]
159
+ for (let i = length - 1; i >= start; i--) {
160
+ const node = childNodes[i]
131
161
 
132
- if (node.nodeType === node.COMMENT_NODE) {
133
- if (node.nodeValue === END) {
134
- end = i
135
- break
136
- }
162
+ if (node.nodeType === node.COMMENT_NODE && node.nodeValue === END) {
163
+ end = i
164
+ break
137
165
  }
138
166
  }
139
167
 
140
- for (let i = start + 1; i <= end; i++) {
141
- childNodes.push(where.childNodes[i])
168
+ // If we can't find the start and end comments, just return all childNodes
169
+ if (start === -1 && end === -1) {
170
+ return Array.from(childNodes)
171
+ }
172
+
173
+ start = start === -1 ? 0 : start
174
+ end = end === -1 ? length - 1 : end
175
+
176
+ const rootChildNodes: Array<Node> = Array(end - start)
177
+
178
+ for (let i = start + 1, j = 0; i <= end; i++) {
179
+ rootChildNodes[j++] = childNodes[i]
142
180
  }
143
181
 
144
- return childNodes
182
+ return rootChildNodes
145
183
  }
146
184
 
147
185
  export function findTemplateResultPartChildNodes(
148
186
  where: ParentChildNodes,
149
187
  parentTemplate: Template,
150
- childTemplate: Template | null,
188
+ childTemplate: Template,
151
189
  partIndex: number,
152
190
  childIndex?: number
153
- ) {
191
+ ): Either.Either<CouldNotFindRootElement | CouldNotFindCommentError, ParentChildNodes> {
154
192
  const [, path] = parentTemplate.parts[partIndex]
155
193
  const parentNode = findPath(where, path) as HTMLElement
156
- const childNodes = findPartChildNodes(parentNode, childTemplate?.hash, partIndex)
194
+ const childNodesEither = findPartChildNodes(parentNode, childTemplate.hash, partIndex)
195
+ if (Either.isLeft(childNodesEither)) return Either.left(childNodesEither.left)
196
+
197
+ const childNodes = childNodesEither.right
157
198
  const parentChildNodes = {
158
199
  parentNode,
159
200
  childNodes: childIndex !== undefined ? [childNodes[childIndex]] : childNodes
160
201
  }
161
202
 
162
- return parentChildNodes
203
+ return Either.right(parentChildNodes)
163
204
  }
164
205
 
165
206
  export function findPartChildNodes(
166
207
  { childNodes }: ParentChildNodes,
167
- hash: string | null | undefined,
208
+ hash: string,
168
209
  partIndex: number
169
- ) {
170
- const [comment, index] = findPartComment(childNodes, partIndex)
171
-
210
+ ): Either.Either<CouldNotFindRootElement | CouldNotFindCommentError, Array<Node>> {
211
+ const either = findPartComment(childNodes, partIndex)
212
+ if (Either.isLeft(either)) return Either.left(either.left)
213
+ const [comment, index] = either.right
172
214
  const nodes: Array<Node> = []
173
215
  const previousHoleValue = `hole${partIndex - 1}`
174
216
 
@@ -183,37 +225,38 @@ export function findPartChildNodes(
183
225
  }
184
226
  }
185
227
  } else {
186
- return [...getPreviousNodes(comment, partIndex), comment]
228
+ return Either.right([...getPreviousNodes(comment, partIndex), comment])
187
229
  }
188
230
 
189
231
  if (nodes.length === 0) {
190
- throw new CouldNotFindRootElement(partIndex)
232
+ return Either.left(new CouldNotFindRootElement(partIndex))
191
233
  }
192
234
 
193
235
  nodes.push(comment)
194
236
 
195
- return nodes
237
+ return Either.right(nodes)
196
238
  }
197
239
 
198
- export function findPartComment(childNodes: ArrayLike<Node>, partIndex: number) {
240
+ export function findPartComment(
241
+ childNodes: ArrayLike<Node>,
242
+ partIndex: number
243
+ ): Either.Either<CouldNotFindCommentError, readonly [Comment, number]> {
199
244
  const search = partIndex === -1 ? `typed-end` : `hole${partIndex}`
200
245
 
201
246
  for (let i = 0; i < childNodes.length; ++i) {
202
247
  const node = childNodes[i]
203
248
 
204
249
  if (isCommentWithValue(node, search)) {
205
- return [node, i] as const
250
+ return Either.right([node, i] as const)
206
251
  }
207
252
  }
208
253
 
209
- throw new CouldNotFindCommentError(partIndex)
254
+ return Either.left(new CouldNotFindCommentError(partIndex))
210
255
  }
211
256
 
212
257
  export function getHydrateEntry({
213
258
  childIndex,
214
259
  document,
215
- eventSource,
216
- onCause,
217
260
  parentTemplate,
218
261
  renderContext,
219
262
  rootIndex,
@@ -226,18 +269,21 @@ export function getHydrateEntry({
226
269
  rootIndex: number
227
270
  parentTemplate: Template | null
228
271
  strings: TemplateStringsArray
229
- eventSource: EventSource
230
- onCause: (cause: Cause.Cause<any>) => Effect.Effect<never, never, void>
231
272
  childIndex?: number
232
- }) {
273
+ }): Either.Either<
274
+ CouldNotFindRootElement | CouldNotFindCommentError,
275
+ { readonly template: Template; readonly wire: Node | Array<Node>; readonly where: ParentChildNodes }
276
+ > {
233
277
  const { template } = getBrowserEntry(document, renderContext, strings)
234
278
 
235
279
  if (parentTemplate) {
236
- where = findTemplateResultPartChildNodes(where, parentTemplate, template, rootIndex, childIndex)
280
+ const either = findTemplateResultPartChildNodes(where, parentTemplate, template, rootIndex, childIndex)
281
+ if (Either.isLeft(either)) {
282
+ return Either.left(either.left)
283
+ }
284
+ where = either.right
237
285
  }
238
286
 
239
- const parts = buildParts(document, renderContext, template, where, eventSource, onCause, true)
240
-
241
287
  const wire = (() => {
242
288
  if (!parentTemplate) {
243
289
  const childNodes = Array.from(where.childNodes).filter((node) =>
@@ -260,10 +306,11 @@ export function getHydrateEntry({
260
306
  )
261
307
  })()
262
308
 
263
- return {
264
- template,
265
- wire,
266
- parts,
267
- where
268
- } as const
309
+ return Either.right(
310
+ {
311
+ template,
312
+ wire,
313
+ where
314
+ } as const
315
+ )
269
316
  }
@@ -4,6 +4,7 @@ import * as Option from "effect/Option"
4
4
  import * as Template from "../Template.js"
5
5
  import type { TextChunk } from "./chunks.js"
6
6
  import {
7
+ getClosingTagName,
7
8
  getPart,
8
9
  getStrictPart,
9
10
  getTextUntilCloseBrace,
@@ -90,6 +91,7 @@ const isSelfClosingTagEndToken: TextPredicate = (input, pos) =>
90
91
  input[pos] === chars.slash && input[pos + 1] === chars.greaterThan
91
92
  const isCommentEndToken: TextPredicate = (input, pos) =>
92
93
  input[pos] === chars.hypen && input[pos + 1] === chars.hypen && input[pos + 2] === chars.greaterThan
94
+ const isGreaterThanToken: TextPredicate = (input, pos) => input[pos] === chars.greaterThan
93
95
 
94
96
  type Context = "unknown" | "element"
95
97
 
@@ -248,8 +250,10 @@ class ParserImpl implements Parser {
248
250
  if (nextChar == "-") {
249
251
  this.consumeAmount(2)
250
252
  return [this.parseComment()]
251
- } else {
253
+ } else if (nextChar.toLowerCase() === "d") {
252
254
  return [this.parseDocType()]
255
+ } else {
256
+ throw new SyntaxError(`Unknown comment type ${nextChar}`)
253
257
  }
254
258
  } else if (nextChar === "/") { // Self-closing tag
255
259
  return this.selfClosingTagEnd()
@@ -312,10 +316,17 @@ class ParserImpl implements Parser {
312
316
  this.path.pop()
313
317
  const element = new Template.ElementNode(tagName, attributes, children)
314
318
 
319
+ this.skipWhitespace()
320
+ this.consumeClosingTag(tagName)
321
+
315
322
  return element
316
323
  }
317
324
  }
318
325
 
326
+ private consumeClosingTag(tagName: string) {
327
+ this.chunk(getClosingTagName(tagName))
328
+ }
329
+
319
330
  private parseSelfClosingElement(tagName: string): Template.SelfClosingElementNode {
320
331
  const attributes = this.parseAttributes()
321
332
 
@@ -352,7 +363,7 @@ class ParserImpl implements Parser {
352
363
  }
353
364
 
354
365
  private parseDocType(): Template.DocType {
355
- this.parseTextUntil((char) => char === chars.greaterThan)
366
+ this.parseTextUntil(isGreaterThanToken)
356
367
  this.consumeAmount(1)
357
368
  this.skipWhitespace()
358
369
 
@@ -523,20 +534,18 @@ class ParserImpl implements Parser {
523
534
  }
524
535
 
525
536
  private parseTextUntil(predicate: (char: string, pos: number) => boolean) {
526
- let text = ""
527
-
537
+ const start = this.pos
538
+ let i = 0
528
539
  while (this.pos < this.length) {
529
- const char = this.nextChar()
530
-
531
540
  if (predicate(this.input, this.pos)) {
532
541
  break
533
542
  }
534
543
 
535
- text += char
544
+ i++
536
545
  this.consumeAmount(1)
537
546
  }
538
547
 
539
- return text
548
+ return this.input.slice(start, start + i)
540
549
  }
541
550
 
542
551
  private parseTextUntilMany<const T extends Predicates>(