@typed/template 0.3.0 → 0.3.2
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.
- package/dist/cjs/internal/EventSource.js +46 -19
- package/dist/cjs/internal/EventSource.js.map +1 -1
- package/dist/cjs/internal/chunks.js +15 -1
- package/dist/cjs/internal/chunks.js.map +1 -1
- package/dist/cjs/internal/errors.js +4 -0
- package/dist/cjs/internal/errors.js.map +1 -1
- package/dist/cjs/internal/hydrate.js +92 -58
- package/dist/cjs/internal/hydrate.js.map +1 -1
- package/dist/cjs/internal/parser.js +14 -6
- package/dist/cjs/internal/parser.js.map +1 -1
- package/dist/cjs/internal/render.js +17 -151
- package/dist/cjs/internal/render.js.map +1 -1
- package/dist/dts/internal/EventSource.d.ts.map +1 -1
- package/dist/dts/internal/chunks.d.ts +1 -0
- package/dist/dts/internal/chunks.d.ts.map +1 -1
- package/dist/dts/internal/errors.d.ts +1 -0
- package/dist/dts/internal/errors.d.ts.map +1 -1
- package/dist/dts/internal/hydrate.d.ts +9 -16
- package/dist/dts/internal/hydrate.d.ts.map +1 -1
- package/dist/dts/internal/parser.d.ts.map +1 -1
- package/dist/dts/internal/render.d.ts +0 -14
- package/dist/dts/internal/render.d.ts.map +1 -1
- package/dist/esm/internal/EventSource.js +51 -21
- package/dist/esm/internal/EventSource.js.map +1 -1
- package/dist/esm/internal/chunks.js +13 -0
- package/dist/esm/internal/chunks.js.map +1 -1
- package/dist/esm/internal/errors.js +3 -0
- package/dist/esm/internal/errors.js.map +1 -1
- package/dist/esm/internal/hydrate.js +94 -56
- package/dist/esm/internal/hydrate.js.map +1 -1
- package/dist/esm/internal/parser.js +16 -7
- package/dist/esm/internal/parser.js.map +1 -1
- package/dist/esm/internal/render.js +18 -147
- package/dist/esm/internal/render.js.map +1 -1
- package/package.json +2 -2
- package/src/internal/EventSource.ts +63 -34
- package/src/internal/chunks.ts +16 -0
- package/src/internal/errors.ts +4 -0
- package/src/internal/hydrate.ts +124 -77
- package/src/internal/parser.ts +17 -8
- package/src/internal/render.ts +30 -293
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Rendered } from "@typed/wire"
|
|
2
2
|
import { Effect, Scope } from "effect"
|
|
3
|
-
import
|
|
3
|
+
import * as Fiber from "effect/Fiber"
|
|
4
4
|
import * as Runtime from "effect/Runtime"
|
|
5
5
|
import { getElements } from "../ElementSource"
|
|
6
6
|
import type { EventHandler } from "../EventHandler"
|
|
@@ -26,31 +26,44 @@ const disposable = (f: () => void): Disposable => ({
|
|
|
26
26
|
[Symbol.dispose]: f
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
const dispose = (d: Disposable): void => d[Symbol.dispose]()
|
|
30
|
+
|
|
29
31
|
export function makeEventSource(): EventSource {
|
|
30
32
|
const bubbleListeners = new Map<
|
|
31
33
|
EventName,
|
|
32
|
-
Set<Entry>
|
|
34
|
+
readonly [normal: Set<Entry>, once: Set<Entry>]
|
|
33
35
|
>()
|
|
34
36
|
const captureListeners = new Map<
|
|
35
37
|
EventName,
|
|
36
|
-
Set<Entry>
|
|
38
|
+
readonly [normal: Set<Entry>, once: Set<Entry>]
|
|
37
39
|
>()
|
|
38
40
|
|
|
39
41
|
function addListener(
|
|
40
42
|
listeners: Map<
|
|
41
43
|
EventName,
|
|
42
|
-
Set<Entry>
|
|
44
|
+
readonly [normal: Set<Entry>, once: Set<Entry>]
|
|
43
45
|
>,
|
|
44
46
|
event: EventName,
|
|
45
47
|
entry: Entry
|
|
46
48
|
): void {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const sets = listeners.get(event)
|
|
50
|
+
const isOnce = entry[1].options?.once === true
|
|
51
|
+
if (sets === undefined) {
|
|
52
|
+
const normal = new Set<Entry>()
|
|
53
|
+
const once = new Set<Entry>()
|
|
54
|
+
if (isOnce) {
|
|
55
|
+
once.add(entry)
|
|
56
|
+
} else {
|
|
57
|
+
normal.add(entry)
|
|
58
|
+
}
|
|
59
|
+
listeners.set(event, [normal, once])
|
|
52
60
|
} else {
|
|
53
|
-
|
|
61
|
+
const [normal, once] = sets
|
|
62
|
+
if (isOnce) {
|
|
63
|
+
once.add(entry)
|
|
64
|
+
} else {
|
|
65
|
+
normal.add(entry)
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
@@ -69,14 +82,16 @@ export function makeEventSource(): EventSource {
|
|
|
69
82
|
function setupBubbleListeners(element: Element, run: Run) {
|
|
70
83
|
const disposables: Array<Disposable> = []
|
|
71
84
|
|
|
72
|
-
for (const [event,
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
for (const [event, sets] of bubbleListeners) {
|
|
86
|
+
for (const handlers of sets) {
|
|
87
|
+
const listener = (ev: Event) =>
|
|
88
|
+
run(
|
|
89
|
+
Effect.forEach(handlers, ([el, handler]) =>
|
|
90
|
+
ev.target === el || el.contains(ev.target as Node) ? handler.handler(ev) : Effect.unit)
|
|
91
|
+
)
|
|
92
|
+
element.addEventListener(event, listener, getDerivedAddEventListenerOptions(handlers))
|
|
93
|
+
disposables.push(disposable(() => element.removeEventListener(event, listener)))
|
|
94
|
+
}
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
return disposables
|
|
@@ -85,16 +100,18 @@ export function makeEventSource(): EventSource {
|
|
|
85
100
|
function setupCaptureListeners(element: Element, run: Run) {
|
|
86
101
|
const disposables: Array<Disposable> = []
|
|
87
102
|
|
|
88
|
-
for (const [event,
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
for (const [event, sets] of captureListeners) {
|
|
104
|
+
for (const handlers of sets) {
|
|
105
|
+
const listener = (ev: Event) =>
|
|
106
|
+
run(
|
|
107
|
+
Effect.forEach(handlers, ([el, handler]) =>
|
|
108
|
+
ev.target === el || el.contains(ev.target as Node)
|
|
109
|
+
? handler.handler(proxyCurrentTargetForCaptureEvents(ev, el))
|
|
110
|
+
: Effect.unit)
|
|
111
|
+
)
|
|
112
|
+
element.addEventListener(event, listener, getDerivedAddEventListenerOptions(handlers))
|
|
113
|
+
disposables.push(disposable(() => element.removeEventListener(event, listener)))
|
|
114
|
+
}
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
return disposables
|
|
@@ -103,17 +120,22 @@ export function makeEventSource(): EventSource {
|
|
|
103
120
|
function setup(rendered: Rendered, scope: Scope.Scope) {
|
|
104
121
|
const hasBubbleListeners = bubbleListeners.size > 0
|
|
105
122
|
const hasCaptureListeners = captureListeners.size > 0
|
|
123
|
+
const elements = getElements(rendered)
|
|
106
124
|
|
|
107
|
-
if (!hasBubbleListeners && !hasCaptureListeners) {
|
|
125
|
+
if (elements.length === 0 || (!hasBubbleListeners && !hasCaptureListeners)) {
|
|
108
126
|
return Effect.unit
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
return Effect.flatMap(Effect.runtime<never>(), (runtime) => {
|
|
112
|
-
const elements = getElements(rendered)
|
|
113
130
|
const disposables: Array<Disposable> = []
|
|
131
|
+
const fibers = new Map<symbol, Fiber.RuntimeFiber<any, any>>()
|
|
114
132
|
const runFork = Runtime.runFork(runtime)
|
|
115
|
-
const run: Run = <E, A>(effect: Effect.Effect<never, E, A>) =>
|
|
116
|
-
|
|
133
|
+
const run: Run = <E, A>(effect: Effect.Effect<never, E, A>) => {
|
|
134
|
+
const id = Symbol()
|
|
135
|
+
const fiber = runFork(Effect.onExit(effect, () => Effect.sync(() => fibers.delete(id))))
|
|
136
|
+
fibers.set(id, fiber)
|
|
137
|
+
return fiber
|
|
138
|
+
}
|
|
117
139
|
|
|
118
140
|
for (const element of elements) {
|
|
119
141
|
if (hasBubbleListeners) {
|
|
@@ -124,7 +146,14 @@ export function makeEventSource(): EventSource {
|
|
|
124
146
|
}
|
|
125
147
|
}
|
|
126
148
|
|
|
127
|
-
return Scope.addFinalizer(
|
|
149
|
+
return Scope.addFinalizer(
|
|
150
|
+
scope,
|
|
151
|
+
Effect.suspend(() => {
|
|
152
|
+
disposables.forEach(dispose)
|
|
153
|
+
if (fibers.size === 0) return Effect.unit
|
|
154
|
+
return Fiber.interruptAll(fibers.values())
|
|
155
|
+
})
|
|
156
|
+
)
|
|
128
157
|
})
|
|
129
158
|
}
|
|
130
159
|
|
|
@@ -147,7 +176,7 @@ function proxyCurrentTargetForCaptureEvents<E extends Event>(event: E, currentTa
|
|
|
147
176
|
function getDerivedAddEventListenerOptions(entries: Set<Entry>): AddEventListenerOptions {
|
|
148
177
|
const hs = Array.from(entries)
|
|
149
178
|
return {
|
|
150
|
-
once: hs.
|
|
179
|
+
once: hs.every((h) => h[1].options?.once === true),
|
|
151
180
|
passive: hs.every((h) => h[1].options?.passive === true)
|
|
152
181
|
}
|
|
153
182
|
}
|
package/src/internal/chunks.ts
CHANGED
|
@@ -55,6 +55,22 @@ export const getAllTextUntilElementClose = (tagName: string) => {
|
|
|
55
55
|
|
|
56
56
|
export const getWhitespace = chunker(/(\s+)/g)
|
|
57
57
|
|
|
58
|
+
const tagNameRegexCache = new Map<string, ReturnType<typeof chunker>>()
|
|
59
|
+
|
|
60
|
+
const tagNameRegex = (tagName: string) => {
|
|
61
|
+
return `(<\\/\\s*${tagName}\\s*>)`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const getClosingTagName = (tagName: string) => {
|
|
65
|
+
const current = tagNameRegexCache.get(tagName)
|
|
66
|
+
if (current !== undefined) {
|
|
67
|
+
return current
|
|
68
|
+
}
|
|
69
|
+
const matcher = chunker(new RegExp(tagNameRegex(tagName), "gi"))
|
|
70
|
+
tagNameRegexCache.set(tagName, matcher)
|
|
71
|
+
return matcher
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
function chunker(regex: RegExp) {
|
|
59
75
|
return (str: string, pos: number): TextChunk | undefined => {
|
|
60
76
|
regex.lastIndex = pos
|
package/src/internal/errors.ts
CHANGED
|
@@ -9,3 +9,7 @@ export class CouldNotFindRootElement extends Error {
|
|
|
9
9
|
super(`Could not find root elements for part ${partIndex}`)
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export function isHydrationError(e: unknown): e is CouldNotFindCommentError | CouldNotFindRootElement {
|
|
14
|
+
return e instanceof CouldNotFindCommentError || e instanceof CouldNotFindRootElement
|
|
15
|
+
}
|
package/src/internal/hydrate.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
17
|
+
import { makeEventSource } from "./EventSource.js"
|
|
18
18
|
import { HydrateContext } from "./HydrateContext.js"
|
|
19
|
-
import {
|
|
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
|
|
59
|
+
const either = getHydrateEntry({
|
|
62
60
|
...hydrateCtx,
|
|
63
61
|
document,
|
|
64
62
|
renderContext,
|
|
65
|
-
|
|
66
|
-
strings: templateStrings,
|
|
67
|
-
onCause: sink.onFailure
|
|
63
|
+
strings: templateStrings
|
|
68
64
|
})
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
71
|
+
const { template, where, wire } = either.right
|
|
89
72
|
|
|
90
|
-
yield* _(
|
|
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
|
-
|
|
144
|
+
let start = -1
|
|
145
|
+
let end = -1
|
|
114
146
|
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
const { childNodes } = where
|
|
148
|
+
const length = childNodes.length
|
|
117
149
|
|
|
118
|
-
for (let i = 0; i <
|
|
119
|
-
const node =
|
|
150
|
+
for (let i = 0; i < length; i++) {
|
|
151
|
+
const node = childNodes[i]
|
|
120
152
|
|
|
121
|
-
if (node.nodeType === node.COMMENT_NODE) {
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
130
|
-
const node =
|
|
159
|
+
for (let i = length - 1; i >= start; i--) {
|
|
160
|
+
const node = childNodes[i]
|
|
131
161
|
|
|
132
|
-
if (node.nodeType === node.COMMENT_NODE) {
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
182
|
+
return rootChildNodes
|
|
145
183
|
}
|
|
146
184
|
|
|
147
185
|
export function findTemplateResultPartChildNodes(
|
|
148
186
|
where: ParentChildNodes,
|
|
149
187
|
parentTemplate: Template,
|
|
150
|
-
childTemplate: Template
|
|
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
|
|
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
|
|
208
|
+
hash: string,
|
|
168
209
|
partIndex: number
|
|
169
|
-
) {
|
|
170
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
309
|
+
return Either.right(
|
|
310
|
+
{
|
|
311
|
+
template,
|
|
312
|
+
wire,
|
|
313
|
+
where
|
|
314
|
+
} as const
|
|
315
|
+
)
|
|
269
316
|
}
|
package/src/internal/parser.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
544
|
+
i++
|
|
536
545
|
this.consumeAmount(1)
|
|
537
546
|
}
|
|
538
547
|
|
|
539
|
-
return
|
|
548
|
+
return this.input.slice(start, start + i)
|
|
540
549
|
}
|
|
541
550
|
|
|
542
551
|
private parseTextUntilMany<const T extends Predicates>(
|