@dom-expressions/runtime 0.50.0-next.15

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.
@@ -0,0 +1,38 @@
1
+ export interface RendererOptions<NodeType> {
2
+ createElement(tag: string): NodeType;
3
+ createTextNode(value: string): NodeType;
4
+ createSentinel?(): NodeType;
5
+ replaceText(textNode: NodeType, value: string): void;
6
+ isTextNode(node: NodeType): boolean;
7
+ setProperty<T>(node: NodeType, name: string, value: T, prev?: T): void;
8
+ insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void;
9
+ removeNode(parent: NodeType, node: NodeType): void;
10
+ cleanupNodes?(parent: NodeType, nodes: NodeType[]): void;
11
+ getParentNode(node: NodeType): NodeType | undefined;
12
+ getFirstChild(node: NodeType): NodeType | undefined;
13
+ getNextSibling(node: NodeType): NodeType | undefined;
14
+ }
15
+
16
+ export interface Renderer<NodeType> {
17
+ render(code: () => NodeType, node: NodeType): () => void;
18
+ effect<T>(fn: (prev?: T) => T, effect: (value: T, prev?: T) => void): void;
19
+ memo<T>(fn: () => T, equal: boolean): () => T;
20
+ createComponent<T>(Comp: (props: T) => NodeType, props: T): NodeType;
21
+ createElement(tag: string): NodeType;
22
+ createTextNode(value: string): NodeType;
23
+ insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void;
24
+ insert<T>(parent: any, accessor: (() => T) | T, marker?: any | null, initial?: any): NodeType;
25
+ spread<T extends object>(node: any, props: T, skipChildren?: boolean): void;
26
+ setProp<T>(node: NodeType, name: string, value: T, prev?: T): T;
27
+ mergeProps(...sources: unknown[]): unknown;
28
+ applyRef(
29
+ r: ((element: NodeType) => void) | ((element: NodeType) => void)[],
30
+ element: NodeType
31
+ ): void;
32
+ ref(
33
+ fn: () => ((element: NodeType) => void) | ((element: NodeType) => void)[],
34
+ element: NodeType
35
+ ): void;
36
+ }
37
+
38
+ export function createRenderer<NodeType>(options: RendererOptions<NodeType>): Renderer<NodeType>;
@@ -0,0 +1,352 @@
1
+ import {
2
+ root,
3
+ effect,
4
+ memo,
5
+ createComponent,
6
+ untrack,
7
+ runWithOwner,
8
+ mergeProps,
9
+ flatten
10
+ } from "rxcore";
11
+
12
+ const INNER_OWNED = {};
13
+
14
+ export function createRenderer({
15
+ createElement,
16
+ createTextNode,
17
+ createSentinel = () => createTextNode(""),
18
+ isTextNode,
19
+ replaceText,
20
+ insertNode,
21
+ removeNode,
22
+ cleanupNodes,
23
+ setProperty,
24
+ getParentNode,
25
+ getFirstChild,
26
+ getNextSibling
27
+ }) {
28
+ function insert(parent, accessor, marker, initial, options) {
29
+ const onUpdate = options && options.onUpdate;
30
+ let effectOptions = options;
31
+ if (onUpdate) {
32
+ const { onUpdate, ...rest } = options;
33
+ effectOptions = rest;
34
+ }
35
+ const multi = marker !== undefined;
36
+ if (multi && !initial) initial = [];
37
+ if (typeof accessor !== "function") {
38
+ accessor = normalize(accessor, multi, true);
39
+ if (typeof accessor !== "function") {
40
+ insertExpression(parent, accessor, initial, marker);
41
+ onUpdate && onUpdate(accessor);
42
+ return;
43
+ }
44
+ }
45
+ if (multi && initial.length === 0) {
46
+ const sentinel = createSentinel();
47
+ insertNode(parent, sentinel, marker);
48
+ initial = [sentinel];
49
+ }
50
+ let current = initial;
51
+ effect(
52
+ prev => {
53
+ const value = normalize(accessor(), multi, true);
54
+ if (typeof value !== "function") return value;
55
+ effect(
56
+ () => normalize(value, multi),
57
+ inner => {
58
+ insertExpression(parent, inner, current, marker);
59
+ current = inner;
60
+ onUpdate && onUpdate(current);
61
+ },
62
+ prev !== undefined && !(options && options.schedule)
63
+ ? { ...effectOptions, schedule: true }
64
+ : effectOptions
65
+ );
66
+ return INNER_OWNED;
67
+ },
68
+ value => {
69
+ if (value === INNER_OWNED) return;
70
+ insertExpression(parent, value, current, marker);
71
+ current = value;
72
+ onUpdate && onUpdate(current);
73
+ },
74
+ effectOptions
75
+ );
76
+ }
77
+
78
+ function insertExpression(parent, value, current, marker) {
79
+ if (value === current) return;
80
+ const t = typeof value,
81
+ multi = marker !== undefined;
82
+
83
+ if (t === "string" || t === "number") {
84
+ const tc = typeof current;
85
+ if (tc === "string" || tc === "number") {
86
+ replaceText(getFirstChild(parent), value);
87
+ } else {
88
+ cleanChildren(parent, current, marker, createTextNode(value));
89
+ }
90
+ } else if (value == null) {
91
+ cleanChildren(parent, current, marker);
92
+ } else if (Array.isArray(value)) {
93
+ if (value.length === 0) {
94
+ cleanChildren(parent, current, marker);
95
+ } else {
96
+ if (Array.isArray(current)) {
97
+ if (current.length === 0) {
98
+ appendNodes(parent, value, marker);
99
+ } else reconcileArrays(parent, current, value);
100
+ } else if (current == null) {
101
+ appendNodes(parent, value);
102
+ } else {
103
+ reconcileArrays(parent, (multi && current) || [getFirstChild(parent)], value);
104
+ }
105
+ }
106
+ } else {
107
+ if (Array.isArray(current)) {
108
+ cleanChildren(parent, current, multi ? marker : null, value);
109
+ } else if (current == null || !getFirstChild(parent)) {
110
+ insertNode(parent, value);
111
+ } else replaceNode(parent, value, getFirstChild(parent));
112
+ }
113
+ }
114
+
115
+ function normalize(value, multi, doNotUnwrap) {
116
+ value = flatten(value, { skipNonRendered: true, doNotUnwrap });
117
+ if (doNotUnwrap && typeof value === "function") return value;
118
+ if (multi && !Array.isArray(value)) value = [value != null ? value : ""];
119
+ if (Array.isArray(value)) {
120
+ for (let i = 0, len = value.length; i < len; i++) {
121
+ const item = value[i],
122
+ t = typeof item;
123
+ if (t === "string" || t === "number") value[i] = createTextNode(item);
124
+ }
125
+ }
126
+ return value;
127
+ }
128
+
129
+ function reconcileArrays(parentNode, a, b) {
130
+ let bLength = b.length,
131
+ aEnd = a.length,
132
+ bEnd = bLength,
133
+ aStart = 0,
134
+ bStart = 0,
135
+ after = getNextSibling(a[aEnd - 1]),
136
+ map = null;
137
+
138
+ while (aStart < aEnd || bStart < bEnd) {
139
+ // common prefix
140
+ if (a[aStart] === b[bStart]) {
141
+ aStart++;
142
+ bStart++;
143
+ continue;
144
+ }
145
+ // common suffix
146
+ while (a[aEnd - 1] === b[bEnd - 1]) {
147
+ aEnd--;
148
+ bEnd--;
149
+ }
150
+ // append
151
+ if (aEnd === aStart) {
152
+ const node =
153
+ bEnd < bLength ? (bStart ? getNextSibling(b[bStart - 1]) : b[bEnd - bStart]) : after;
154
+
155
+ while (bStart < bEnd) insertNode(parentNode, b[bStart++], node);
156
+ // remove
157
+ } else if (bEnd === bStart) {
158
+ while (aStart < aEnd) {
159
+ if (!map || !map.has(a[aStart])) removeNode(parentNode, a[aStart]);
160
+ aStart++;
161
+ }
162
+ // swap backward — symmetric end-swap detected. Walk inward with a single
163
+ // stable front anchor (a[aStart]); each move targets the same host
164
+ // position and avoids cross-anchored inserts on reorder-heavy patterns.
165
+ } else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
166
+ const anchor = a[aStart];
167
+ do {
168
+ insertNode(parentNode, a[--aEnd], anchor);
169
+ bStart++;
170
+ if (aStart >= aEnd - 1 || bStart >= bEnd) break;
171
+ } while (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]);
172
+ // fallback to map
173
+ } else {
174
+ if (!map) {
175
+ map = new Map();
176
+ let i = bStart;
177
+
178
+ while (i < bEnd) map.set(b[i], i++);
179
+ }
180
+
181
+ const index = map.get(a[aStart]);
182
+ if (index != null) {
183
+ if (bStart < index && index < bEnd) {
184
+ let i = aStart,
185
+ sequence = 1,
186
+ t;
187
+
188
+ while (++i < aEnd && i < bEnd) {
189
+ if ((t = map.get(a[i])) == null || t !== index + sequence) break;
190
+ sequence++;
191
+ }
192
+
193
+ if (sequence > index - bStart) {
194
+ const node = a[aStart];
195
+ while (bStart < index) insertNode(parentNode, b[bStart++], node);
196
+ } else replaceNode(parentNode, b[bStart++], a[aStart++]);
197
+ } else aStart++;
198
+ } else removeNode(parentNode, a[aStart++]);
199
+ }
200
+ }
201
+ }
202
+
203
+ function cleanChildren(parent, current, marker, replacement) {
204
+ if (marker === undefined) {
205
+ let removed;
206
+ while ((removed = getFirstChild(parent))) removeNode(parent, removed);
207
+ replacement && insertNode(parent, replacement);
208
+ return "";
209
+ }
210
+ if (current.length) {
211
+ let inserted = false;
212
+ for (let i = current.length - 1; i >= 0; i--) {
213
+ const el = current[i];
214
+ if (replacement !== el) {
215
+ const isParent = getParentNode(el) === parent;
216
+ if (replacement && !inserted && !i)
217
+ isParent
218
+ ? replaceNode(parent, replacement, el)
219
+ : insertNode(parent, replacement, marker);
220
+ else isParent && removeNode(parent, el);
221
+ } else inserted = true;
222
+ }
223
+ } else if (replacement) insertNode(parent, replacement, marker);
224
+ }
225
+
226
+ function appendNodes(parent, array, marker) {
227
+ for (let i = 0, len = array.length; i < len; i++) insertNode(parent, array[i], marker);
228
+ }
229
+
230
+ function replaceNode(parent, newNode, oldNode) {
231
+ insertNode(parent, newNode, oldNode);
232
+ removeNode(parent, oldNode);
233
+ }
234
+
235
+ function collectNodes(value, nodes) {
236
+ if (Array.isArray(value)) {
237
+ for (let i = 0, len = value.length; i < len; i++) collectNodes(value[i], nodes);
238
+ } else if (value != null && typeof value !== "string" && typeof value !== "number") {
239
+ nodes.push(value);
240
+ }
241
+ return nodes;
242
+ }
243
+
244
+ function collectMounted(parent, value) {
245
+ const nodes = collectNodes(value, []);
246
+ if (!nodes.length && (typeof value === "string" || typeof value === "number")) {
247
+ const node = getFirstChild(parent);
248
+ if (node) nodes.push(node);
249
+ }
250
+ return nodes;
251
+ }
252
+
253
+ function defaultCleanupNodes(parent, nodes) {
254
+ for (let i = 0, len = nodes.length; i < len; i++) {
255
+ const node = nodes[i];
256
+ if (getParentNode(node) === parent) removeNode(parent, node);
257
+ }
258
+ }
259
+
260
+ // TODO: make this better
261
+ function spread(node, props, skipChildren) {
262
+ const prevProps = {};
263
+ props || (props = {});
264
+ if (!skipChildren) insert(node, () => props.children);
265
+ effect(
266
+ () => {
267
+ const r = props.ref;
268
+ (typeof r === "function" || Array.isArray(r)) && ref(() => r, node);
269
+ },
270
+ () => {}
271
+ );
272
+ effect(
273
+ () => {
274
+ const newProps = {};
275
+ for (const prop in props) {
276
+ if (prop === "children" || prop === "ref") continue;
277
+ newProps[prop] = props[prop];
278
+ }
279
+ return newProps;
280
+ },
281
+ props => {
282
+ for (const prop in prevProps) {
283
+ if (!(prop in props)) {
284
+ setProperty(node, prop, undefined, prevProps[prop]);
285
+ delete prevProps[prop];
286
+ }
287
+ }
288
+ for (const prop in props) {
289
+ const value = props[prop];
290
+ if (value === prevProps[prop]) continue;
291
+ setProperty(node, prop, value, prevProps[prop]);
292
+ prevProps[prop] = value;
293
+ }
294
+ }
295
+ );
296
+ return prevProps;
297
+ }
298
+
299
+ function applyRef(r, element) {
300
+ Array.isArray(r) ? r.flat(Infinity).forEach(f => f && f(element)) : r(element);
301
+ }
302
+
303
+ function ref(fn, element) {
304
+ const resolved = untrack(fn);
305
+ runWithOwner(null, () => applyRef(resolved, element));
306
+ }
307
+
308
+ return {
309
+ render(code, element) {
310
+ let disposer,
311
+ disposed = false,
312
+ mounted = [];
313
+ const cleanup = cleanupNodes || defaultCleanupNodes;
314
+ try {
315
+ root(dispose => {
316
+ disposer = dispose;
317
+ insert(element, code(), undefined, undefined, {
318
+ onUpdate(value) {
319
+ mounted = collectMounted(element, value);
320
+ }
321
+ });
322
+ });
323
+ } catch (err) {
324
+ if (disposer) disposer();
325
+ cleanup(element, mounted);
326
+ throw err;
327
+ }
328
+ return () => {
329
+ if (disposed) return;
330
+ disposed = true;
331
+ disposer();
332
+ cleanup(element, mounted);
333
+ mounted = [];
334
+ };
335
+ },
336
+ insert,
337
+ spread,
338
+ createElement,
339
+ createTextNode,
340
+ insertNode,
341
+ setProp(node, name, value, prev) {
342
+ setProperty(node, name, value, prev);
343
+ return value;
344
+ },
345
+ mergeProps,
346
+ effect,
347
+ memo,
348
+ createComponent,
349
+ applyRef,
350
+ ref
351
+ };
352
+ }