a post-component library for building user-interfaces on the web.
1import {
2 assert,
3 is_html,
4 is_iterable,
5 is_keyed,
6 is_renderable,
7 single_part_template,
8 type Displayable,
9 type Key,
10 type Renderable,
11} from '../shared.ts'
12import {
13 compile_template,
14 DYNAMIC_WHOLE,
15 PART_ATTRIBUTE,
16 PART_CHILD,
17 PART_DIRECTIVE,
18 PART_PROPERTY,
19 type CompiledTemplate,
20} from './compiler.ts'
21import {
22 create_attribute_part,
23 create_child_part,
24 create_directive_part,
25 create_property_part,
26 type Part,
27} from './parts.ts'
28import { create_span_into, type Span } from './span.ts'
29import { is_comment, is_element } from './util.ts'
30
31export interface Root {
32 render(value: Displayable): void
33}
34
35export function createRoot(parent: Node): Root {
36 const span = create_span_into(parent)
37 return { render: create_child_part(span) }
38}
39
40function find_end(start: Comment): Comment | null {
41 assert(start.data === '?[')
42 let depth = 1
43 let node: ChildNode | null = start
44 while ((node = node.nextSibling)) {
45 if (is_comment(node)) {
46 if (node.data === '?[') depth++
47 else if (node.data === '?]') {
48 depth--
49 if (depth === 0) return node
50 }
51 }
52 }
53 return null
54}
55
56export function hydrate(parent: Node, value: Displayable): Root {
57 let start
58 for (start of parent.childNodes) {
59 if (is_comment(start) && start.data === '?[') break
60 }
61 assert(
62 start && is_comment(start),
63 `Could not find hydration start comment. Please ensure the element contains server-side rendered output.`,
64 )
65
66 const end = find_end(start)
67 assert(end, `Could not find hydration end comment. Please ensure the element contains server-side rendered output.`)
68
69 const render = hydrate_child_part({ _start: start, _end: end }, value)
70 render(value)
71 return { render }
72}
73
74function hydrate_child_part(span: Span, value: unknown) {
75 let current_renderable: Renderable | undefined
76 let template: CompiledTemplate | undefined
77 let template_parts: [number, Part][] | undefined
78 let entries: Array<{ _span: Span; _part: Part; _key: Key }> | undefined
79
80 if (is_renderable(value)) {
81 try {
82 value = (current_renderable = value).render()
83 } catch (thrown) {
84 if (is_html(thrown)) {
85 value = thrown
86 } else {
87 throw thrown
88 }
89 }
90
91 if (is_renderable(value)) value = single_part_template(value)
92 }
93
94 if (is_iterable(value)) {
95 entries = []
96 let end = span._start
97
98 for (const item of value) {
99 const key = is_keyed(item) ? item._key : (item as Key)
100
101 const start = end.nextSibling
102 assert(start && is_comment(start) && start.data === '?[')
103
104 end = find_end(start)!
105 assert(end)
106
107 const span = { _start: start, _end: end }
108 entries.push({ _span: span, _part: hydrate_child_part(span, item), _key: key })
109 }
110 assert(end.nextSibling === span._end)
111 }
112
113 if (is_html(value)) {
114 template = compile_template(value._statics)
115
116 const node_by_part: Array<Node | Span> = []
117
118 const walker = document.createTreeWalker(span._start.parentNode!, 129)
119 const template_walker = document.createTreeWalker(template._content, 129)
120 assert((NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT) === 129)
121 walker.currentNode = span._start
122
123 while (walker.nextNode() && template_walker.nextNode()) {
124 const node = walker.currentNode
125 const template_node = template_walker.currentNode
126 if (node === span._end) break
127
128 if (is_comment(node) && is_comment(template_node)) {
129 if (node.data === '?[') {
130 assert(DYNAMIC_WHOLE.test(template_node.data))
131 const end = find_end(node)
132 assert(end)
133 walker.currentNode = end
134 }
135 continue
136 }
137
138 assert(is_element(node))
139 assert(
140 node.nodeType === template_node.nodeType,
141 `Node type mismatch: ${node.nodeType} != ${template_node.nodeType}`,
142 )
143 assert(template_node instanceof HTMLElement || template_node instanceof SVGElement)
144 assert(node.tagName === template_node.tagName, `Tag name mismatch: ${node.tagName} !== ${template_node.tagName}`)
145
146 if (template_node.dataset.dynparts)
147 for (const part of template_node.dataset.dynparts.split(' ')) node_by_part[+part] = node
148 }
149
150 for (const part of template._root_parts) node_by_part[part] = span
151
152 template_parts = template._parts.map(([dynamic_index, [type, data]], element_index): [number, Part] => {
153 const node = node_by_part[element_index]
154 switch (type) {
155 case PART_CHILD:
156 let child: ChildNode | null
157
158 if (node instanceof Node) {
159 child = node.childNodes[data]
160 assert(child)
161 } else {
162 child = node._start.nextSibling
163 assert(child)
164 for (let i = 0; i < data; i++) {
165 child = child.nextSibling
166 assert(child !== null, 'expected more siblings')
167 assert(child !== node._end, 'ran out of siblings before the end')
168 }
169 }
170
171 assert(child.parentNode)
172 assert(child.previousSibling && is_comment(child.previousSibling) && child.previousSibling.data === '?[')
173 const end = find_end(child.previousSibling)
174 assert(end)
175
176 return [
177 dynamic_index,
178 hydrate_child_part(
179 {
180 _start: child.previousSibling,
181 _end: end,
182 },
183 value._dynamics[dynamic_index],
184 ),
185 ]
186 case PART_DIRECTIVE:
187 assert(node instanceof Node)
188 return [dynamic_index, create_directive_part(node)]
189 case PART_ATTRIBUTE:
190 assert(node instanceof Element)
191 return [dynamic_index, create_attribute_part(node, data)]
192 case PART_PROPERTY:
193 assert(node instanceof Node)
194 return [dynamic_index, create_property_part(node, data)]
195 }
196 })
197 }
198
199 return create_child_part(span, true, current_renderable, template, template_parts, entries, value)
200}