@b9g/crank 0.5.7 → 0.6.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.
package/umd.js CHANGED
@@ -4,2278 +4,2333 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Crank = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- const NOOP = () => { };
8
- const IDENTITY = (value) => value;
9
- function wrap(value) {
10
- return value === undefined ? [] : Array.isArray(value) ? value : [value];
11
- }
12
- function unwrap(arr) {
13
- return arr.length === 0 ? undefined : arr.length === 1 ? arr[0] : arr;
14
- }
15
- /**
16
- * Ensures a value is an array.
17
- *
18
- * This function does the same thing as wrap() above except it handles nulls
19
- * and iterables, so it is appropriate for wrapping user-provided element
20
- * children.
21
- */
22
- function arrayify(value) {
23
- return value == null
24
- ? []
25
- : Array.isArray(value)
26
- ? value
27
- : typeof value === "string" ||
28
- typeof value[Symbol.iterator] !== "function"
29
- ? [value]
30
- : // TODO: inference broke in TypeScript 3.9.
31
- [...value];
32
- }
33
- function isIteratorLike(value) {
34
- return value != null && typeof value.next === "function";
35
- }
36
- function isPromiseLike(value) {
37
- return value != null && typeof value.then === "function";
38
- }
39
- /***
40
- * SPECIAL TAGS
41
- *
42
- * Crank provides a couple tags which have special meaning for the renderer.
43
- ***/
44
- /**
45
- * A special tag for grouping multiple children within the same parent.
46
- *
47
- * All non-string iterables which appear in the element tree are implicitly
48
- * wrapped in a fragment element.
49
- *
50
- * This tag is just the empty string, and you can use the empty string in
51
- * createElement calls or transpiler options directly to avoid having to
52
- * reference this export.
53
- */
54
- const Fragment = "";
55
- // TODO: We assert the following symbol tags as any because TypeScript support
56
- // for symbol tags in JSX doesn’t exist yet.
57
- // https://github.com/microsoft/TypeScript/issues/38367
58
- /**
59
- * A special tag for rendering into a new root node via a root prop.
60
- *
61
- * This tag is useful for creating element trees with multiple roots, for
62
- * things like modals or tooltips.
63
- *
64
- * Renderer.prototype.render() will implicitly wrap top-level element trees in
65
- * a Portal element.
66
- */
67
- const Portal = Symbol.for("crank.Portal");
68
- /**
69
- * A special tag which preserves whatever was previously rendered in the
70
- * element’s position.
71
- *
72
- * Copy elements are useful for when you want to prevent a subtree from
73
- * rerendering as a performance optimization. Copy elements can also be keyed,
74
- * in which case the previously rendered keyed element will be copied.
75
- */
76
- const Copy = Symbol.for("crank.Copy");
77
- /**
78
- * A special tag for injecting raw nodes or strings via a value prop.
79
- *
80
- * Renderer.prototype.raw() is called with the value prop.
81
- */
82
- const Raw = Symbol.for("crank.Raw");
83
- const ElementSymbol = Symbol.for("crank.Element");
84
- /**
85
- * Elements are the basic building blocks of Crank applications. They are
86
- * JavaScript objects which are interpreted by special classes called renderers
87
- * to produce and manage stateful nodes.
88
- *
89
- * @template {Tag} [TTag=Tag] - The type of the tag of the element.
90
- *
91
- * @example
92
- * // specific element types
93
- * let div: Element<"div">;
94
- * let portal: Element<Portal>;
95
- * let myEl: Element<MyComponent>;
96
- *
97
- * // general element types
98
- * let host: Element<string | symbol>;
99
- * let component: Element<Component>;
100
- *
101
- * Typically, you use a helper function like createElement to create elements
102
- * rather than instatiating this class directly.
103
- */
104
- class Element {
105
- constructor(tag, props, key, ref, static_) {
106
- this.tag = tag;
107
- this.props = props;
108
- this.key = key;
109
- this.ref = ref;
110
- this.static_ = static_;
111
- }
112
- }
113
- // See Element interface
114
- Element.prototype.$$typeof = ElementSymbol;
115
- function isElement(value) {
116
- return value != null && value.$$typeof === ElementSymbol;
117
- }
118
- /**
119
- * Creates an element with the specified tag, props and children.
120
- *
121
- * This function is usually used as a transpilation target for JSX transpilers,
122
- * but it can also be called directly. It additionally extracts special props so
123
- * they aren’t accessible to renderer methods or components, and assigns the
124
- * children prop according to any additional arguments passed to the function.
125
- */
126
- function createElement(tag, props, ...children) {
127
- let key;
128
- let ref;
129
- let static_ = false;
130
- const props1 = {};
131
- if (props != null) {
132
- for (const name in props) {
133
- switch (name) {
134
- case "crank-key":
135
- case "c-key":
136
- case "$key":
137
- // We have to make sure we don’t assign null to the key because we
138
- // don’t check for null keys in the diffing functions.
139
- if (props[name] != null) {
140
- key = props[name];
141
- }
142
- break;
143
- case "crank-ref":
144
- case "c-ref":
145
- case "$ref":
146
- if (typeof props[name] === "function") {
147
- ref = props[name];
148
- }
149
- break;
150
- case "crank-static":
151
- case "c-static":
152
- case "$static":
153
- static_ = !!props[name];
154
- break;
155
- default:
156
- props1[name] = props[name];
157
- }
158
- }
159
- }
160
- if (children.length > 1) {
161
- props1.children = children;
162
- }
163
- else if (children.length === 1) {
164
- props1.children = children[0];
165
- }
166
- return new Element(tag, props1, key, ref, static_);
167
- }
168
- /** Clones a given element, shallowly copying the props object. */
169
- function cloneElement(el) {
170
- if (!isElement(el)) {
171
- throw new TypeError("Cannot clone non-element");
172
- }
173
- return new Element(el.tag, { ...el.props }, el.key, el.ref);
174
- }
175
- function narrow(value) {
176
- if (typeof value === "boolean" || value == null) {
177
- return undefined;
178
- }
179
- else if (typeof value === "string" || isElement(value)) {
180
- return value;
181
- }
182
- else if (typeof value[Symbol.iterator] === "function") {
183
- return createElement(Fragment, null, value);
184
- }
185
- return value.toString();
186
- }
187
- /**
188
- * Takes an array of element values and normalizes the output as an array of
189
- * nodes and strings.
190
- *
191
- * @returns Normalized array of nodes and/or strings.
192
- *
193
- * Normalize will flatten only one level of nested arrays, because it is
194
- * designed to be called once at each level of the tree. It will also
195
- * concatenate adjacent strings and remove all undefined values.
196
- */
197
- function normalize(values) {
198
- const result = [];
199
- let buffer;
200
- for (let i = 0; i < values.length; i++) {
201
- const value = values[i];
202
- if (!value) ;
203
- else if (typeof value === "string") {
204
- buffer = (buffer || "") + value;
205
- }
206
- else if (!Array.isArray(value)) {
207
- if (buffer) {
208
- result.push(buffer);
209
- buffer = undefined;
210
- }
211
- result.push(value);
212
- }
213
- else {
214
- // We could use recursion here but it’s just easier to do it inline.
215
- for (let j = 0; j < value.length; j++) {
216
- const value1 = value[j];
217
- if (!value1) ;
218
- else if (typeof value1 === "string") {
219
- buffer = (buffer || "") + value1;
220
- }
221
- else {
222
- if (buffer) {
223
- result.push(buffer);
224
- buffer = undefined;
225
- }
226
- result.push(value1);
227
- }
228
- }
229
- }
230
- }
231
- if (buffer) {
232
- result.push(buffer);
233
- }
234
- return result;
235
- }
236
- /**
237
- * @internal
238
- * The internal nodes which are cached and diffed against new elements when
239
- * rendering element trees.
240
- */
241
- class Retainer {
242
- constructor(el) {
243
- this.el = el;
244
- this.ctx = undefined;
245
- this.children = undefined;
246
- this.value = undefined;
247
- this.cachedChildValues = undefined;
248
- this.fallbackValue = undefined;
249
- this.inflightValue = undefined;
250
- this.onNextValues = undefined;
251
- }
252
- }
253
- /**
254
- * Finds the value of the element according to its type.
255
- *
256
- * @returns The value of the element.
257
- */
258
- function getValue(ret) {
259
- if (typeof ret.fallbackValue !== "undefined") {
260
- return typeof ret.fallbackValue === "object"
261
- ? getValue(ret.fallbackValue)
262
- : ret.fallbackValue;
263
- }
264
- else if (ret.el.tag === Portal) {
265
- return;
266
- }
267
- else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) {
268
- return ret.value;
269
- }
270
- return unwrap(getChildValues(ret));
271
- }
272
- /**
273
- * Walks an element’s children to find its child values.
274
- *
275
- * @returns A normalized array of nodes and strings.
276
- */
277
- function getChildValues(ret) {
278
- if (ret.cachedChildValues) {
279
- return wrap(ret.cachedChildValues);
280
- }
281
- const values = [];
282
- const children = wrap(ret.children);
283
- for (let i = 0; i < children.length; i++) {
284
- const child = children[i];
285
- if (child) {
286
- values.push(typeof child === "string" ? child : getValue(child));
287
- }
288
- }
289
- const values1 = normalize(values);
290
- const tag = ret.el.tag;
291
- if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
292
- ret.cachedChildValues = unwrap(values1);
293
- }
294
- return values1;
295
- }
296
- const defaultRendererImpl = {
297
- create() {
298
- throw new Error("Not implemented");
299
- },
300
- hydrate() {
301
- throw new Error("Not implemented");
302
- },
303
- scope: IDENTITY,
304
- read: IDENTITY,
305
- text: IDENTITY,
306
- raw: IDENTITY,
307
- patch: NOOP,
308
- arrange: NOOP,
309
- dispose: NOOP,
310
- flush: NOOP,
311
- };
312
- const _RendererImpl = Symbol.for("crank.RendererImpl");
313
- /**
314
- * An abstract class which is subclassed to render to different target
315
- * environments. This class is responsible for kicking off the rendering
316
- * process and caching previous trees by root.
317
- *
318
- * @template TNode - The type of the node for a rendering environment.
319
- * @template TScope - Data which is passed down the tree.
320
- * @template TRoot - The type of the root for a rendering environment.
321
- * @template TResult - The type of exposed values.
322
- */
323
- class Renderer {
324
- constructor(impl) {
325
- this.cache = new WeakMap();
326
- this[_RendererImpl] = {
327
- ...defaultRendererImpl,
328
- ...impl,
329
- };
330
- }
331
- /**
332
- * Renders an element tree into a specific root.
333
- *
334
- * @param children - An element tree. You can render null with a previously
335
- * used root to delete the previously rendered element tree from the cache.
336
- * @param root - The node to be rendered into. The renderer will cache
337
- * element trees per root.
338
- * @param bridge - An optional context that will be the ancestor context of all
339
- * elements in the tree. Useful for connecting different renderers so that
340
- * events/provisions properly propagate. The context for a given root must be
341
- * the same or an error will be thrown.
342
- *
343
- * @returns The result of rendering the children, or a possible promise of
344
- * the result if the element tree renders asynchronously.
345
- */
346
- render(children, root, bridge) {
347
- let ret;
348
- const ctx = bridge && bridge[_ContextImpl];
349
- if (typeof root === "object" && root !== null) {
350
- ret = this.cache.get(root);
351
- }
352
- let oldProps;
353
- if (ret === undefined) {
354
- ret = new Retainer(createElement(Portal, { children, root }));
355
- ret.value = root;
356
- ret.ctx = ctx;
357
- if (typeof root === "object" && root !== null && children != null) {
358
- this.cache.set(root, ret);
359
- }
360
- }
361
- else if (ret.ctx !== ctx) {
362
- throw new Error("Context mismatch");
363
- }
364
- else {
365
- oldProps = ret.el.props;
366
- ret.el = createElement(Portal, { children, root });
367
- if (typeof root === "object" && root !== null && children == null) {
368
- this.cache.delete(root);
369
- }
370
- }
371
- const impl = this[_RendererImpl];
372
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined);
373
- // We return the child values of the portal because portal elements
374
- // themselves have no readable value.
375
- if (isPromiseLike(childValues)) {
376
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
377
- }
378
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
379
- }
380
- hydrate(children, root, bridge) {
381
- const impl = this[_RendererImpl];
382
- const ctx = bridge && bridge[_ContextImpl];
383
- let ret;
384
- ret = this.cache.get(root);
385
- if (ret !== undefined) {
386
- // If there is a retainer for the root, hydration is not necessary.
387
- return this.render(children, root, bridge);
388
- }
389
- let oldProps;
390
- ret = new Retainer(createElement(Portal, { children, root }));
391
- ret.value = root;
392
- if (typeof root === "object" && root !== null && children != null) {
393
- this.cache.set(root, ret);
394
- }
395
- const hydrationData = impl.hydrate(Portal, root, {});
396
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData);
397
- // We return the child values of the portal because portal elements
398
- // themselves have no readable value.
399
- if (isPromiseLike(childValues)) {
400
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
401
- }
402
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
403
- }
404
- }
405
- /*** PRIVATE RENDERER FUNCTIONS ***/
406
- function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
407
- // element is a host or portal element
408
- if (root != null) {
409
- renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
410
- flush(renderer, root);
411
- }
412
- ret.cachedChildValues = unwrap(childValues);
413
- if (root == null) {
414
- unmount(renderer, ret, ctx, ret);
415
- }
416
- return renderer.read(ret.cachedChildValues);
417
- }
418
- function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) {
419
- const oldRetained = wrap(parent.children);
420
- const newRetained = [];
421
- const newChildren = arrayify(children);
422
- const values = [];
423
- let graveyard;
424
- let childrenByKey;
425
- let seenKeys;
426
- let isAsync = false;
427
- let hydrationBlock;
428
- let oi = 0;
429
- let oldLength = oldRetained.length;
430
- for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) {
431
- // length checks to prevent index out of bounds deoptimizations.
432
- let ret = oi >= oldLength ? undefined : oldRetained[oi];
433
- let child = narrow(newChildren[ni]);
434
- {
435
- // aligning new children with old retainers
436
- let oldKey = typeof ret === "object" ? ret.el.key : undefined;
437
- let newKey = typeof child === "object" ? child.key : undefined;
438
- if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) {
439
- console.error("Duplicate key", newKey);
440
- newKey = undefined;
441
- }
442
- if (oldKey === newKey) {
443
- if (childrenByKey !== undefined && newKey !== undefined) {
444
- childrenByKey.delete(newKey);
445
- }
446
- oi++;
447
- }
448
- else {
449
- childrenByKey = childrenByKey || createChildrenByKey(oldRetained, oi);
450
- if (newKey === undefined) {
451
- while (ret !== undefined && oldKey !== undefined) {
452
- oi++;
453
- ret = oldRetained[oi];
454
- oldKey = typeof ret === "object" ? ret.el.key : undefined;
455
- }
456
- oi++;
457
- }
458
- else {
459
- ret = childrenByKey.get(newKey);
460
- if (ret !== undefined) {
461
- childrenByKey.delete(newKey);
462
- }
463
- (seenKeys = seenKeys || new Set()).add(newKey);
464
- }
465
- }
466
- }
467
- // Updating
468
- let value;
469
- if (typeof child === "object") {
470
- if (child.tag === Copy) {
471
- value = getInflightValue(ret);
472
- }
473
- else {
474
- let oldProps;
475
- let static_ = false;
476
- if (typeof ret === "object" && ret.el.tag === child.tag) {
477
- oldProps = ret.el.props;
478
- ret.el = child;
479
- if (child.static_) {
480
- value = getInflightValue(ret);
481
- static_ = true;
482
- }
483
- }
484
- else {
485
- if (typeof ret === "object") {
486
- (graveyard = graveyard || []).push(ret);
487
- }
488
- const fallback = ret;
489
- ret = new Retainer(child);
490
- ret.fallbackValue = fallback;
491
- }
492
- if (static_) ;
493
- else if (child.tag === Raw) {
494
- value = hydrationBlock
495
- ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData))
496
- : updateRaw(renderer, ret, scope, oldProps, hydrationData);
497
- }
498
- else if (child.tag === Fragment) {
499
- value = hydrationBlock
500
- ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData))
501
- : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData);
502
- }
503
- else if (typeof child.tag === "function") {
504
- value = hydrationBlock
505
- ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData))
506
- : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData);
507
- }
508
- else {
509
- value = hydrationBlock
510
- ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData))
511
- : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData);
512
- }
513
- }
514
- const ref = child.ref;
515
- if (isPromiseLike(value)) {
516
- isAsync = true;
517
- if (typeof ref === "function") {
518
- value = value.then((value) => {
519
- ref(renderer.read(value));
520
- return value;
521
- });
522
- }
523
- if (hydrationData !== undefined) {
524
- hydrationBlock = value;
525
- }
526
- }
527
- else {
528
- if (typeof ref === "function") {
529
- ref(renderer.read(value));
530
- }
531
- }
532
- }
533
- else {
534
- // child is a string or undefined
535
- if (typeof ret === "object") {
536
- (graveyard = graveyard || []).push(ret);
537
- }
538
- if (typeof child === "string") {
539
- value = ret = renderer.text(child, scope, hydrationData);
540
- }
541
- else {
542
- ret = undefined;
543
- }
544
- }
545
- values[ni] = value;
546
- newRetained[ni] = ret;
547
- }
548
- // cleanup remaining retainers
549
- for (; oi < oldLength; oi++) {
550
- const ret = oldRetained[oi];
551
- if (typeof ret === "object" &&
552
- (typeof ret.el.key === "undefined" ||
553
- !seenKeys ||
554
- !seenKeys.has(ret.el.key))) {
555
- (graveyard = graveyard || []).push(ret);
556
- }
557
- }
558
- if (childrenByKey !== undefined && childrenByKey.size > 0) {
559
- (graveyard = graveyard || []).push(...childrenByKey.values());
560
- }
561
- parent.children = unwrap(newRetained);
562
- if (isAsync) {
563
- let childValues1 = Promise.all(values).finally(() => {
564
- if (graveyard) {
565
- for (let i = 0; i < graveyard.length; i++) {
566
- unmount(renderer, host, ctx, graveyard[i]);
567
- }
568
- }
569
- });
570
- let onChildValues;
571
- childValues1 = Promise.race([
572
- childValues1,
573
- new Promise((resolve) => (onChildValues = resolve)),
574
- ]);
575
- if (parent.onNextValues) {
576
- parent.onNextValues(childValues1);
577
- }
578
- parent.onNextValues = onChildValues;
579
- return childValues1.then((childValues) => {
580
- parent.inflightValue = parent.fallbackValue = undefined;
581
- return normalize(childValues);
582
- });
583
- }
584
- else {
585
- if (graveyard) {
586
- for (let i = 0; i < graveyard.length; i++) {
587
- unmount(renderer, host, ctx, graveyard[i]);
588
- }
589
- }
590
- if (parent.onNextValues) {
591
- parent.onNextValues(values);
592
- parent.onNextValues = undefined;
593
- }
594
- parent.inflightValue = parent.fallbackValue = undefined;
595
- // We can assert there are no promises in the array because isAsync is false
596
- return normalize(values);
597
- }
598
- }
599
- function createChildrenByKey(children, offset) {
600
- const childrenByKey = new Map();
601
- for (let i = offset; i < children.length; i++) {
602
- const child = children[i];
603
- if (typeof child === "object" && typeof child.el.key !== "undefined") {
604
- childrenByKey.set(child.el.key, child);
605
- }
606
- }
607
- return childrenByKey;
608
- }
609
- function getInflightValue(child) {
610
- if (typeof child !== "object") {
611
- return child;
612
- }
613
- const ctx = typeof child.el.tag === "function" ? child.ctx : undefined;
614
- if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
615
- return ctx.inflightValue;
616
- }
617
- else if (child.inflightValue) {
618
- return child.inflightValue;
619
- }
620
- return getValue(child);
621
- }
622
- function updateRaw(renderer, ret, scope, oldProps, hydrationData) {
623
- const props = ret.el.props;
624
- if (!oldProps || oldProps.value !== props.value) {
625
- ret.value = renderer.raw(props.value, scope, hydrationData);
626
- }
627
- return ret.value;
628
- }
629
- function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) {
630
- const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData);
631
- if (isPromiseLike(childValues)) {
632
- ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
633
- return ret.inflightValue;
634
- }
635
- return unwrap(childValues);
636
- }
637
- function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) {
638
- const el = ret.el;
639
- const tag = el.tag;
640
- let hydrationValue;
641
- if (el.tag === Portal) {
642
- root = ret.value = el.props.root;
643
- }
644
- else {
645
- if (hydrationData !== undefined) {
646
- const value = hydrationData.children.shift();
647
- hydrationValue = value;
648
- }
649
- }
650
- scope = renderer.scope(scope, tag, el.props);
651
- let childHydrationData;
652
- if (hydrationValue != null && typeof hydrationValue !== "string") {
653
- childHydrationData = renderer.hydrate(tag, hydrationValue, el.props);
654
- if (childHydrationData === undefined) {
655
- hydrationValue = undefined;
656
- }
657
- }
658
- const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData);
659
- if (isPromiseLike(childValues)) {
660
- ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue));
661
- return ret.inflightValue;
662
- }
663
- return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue);
664
- }
665
- function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) {
666
- const tag = ret.el.tag;
667
- let value = ret.value;
668
- if (hydrationValue != null) {
669
- value = ret.value = hydrationValue;
670
- }
671
- let props = ret.el.props;
672
- let copied;
673
- if (tag !== Portal) {
674
- if (value == null) {
675
- // This assumes that renderer.create does not return nullish values.
676
- value = ret.value = renderer.create(tag, props, scope);
677
- }
678
- for (const propName in { ...oldProps, ...props }) {
679
- const propValue = props[propName];
680
- if (propValue === Copy) {
681
- // TODO: The Copy tag doubles as a way to skip the patching of a prop.
682
- // Not sure about this feature. Should probably be removed.
683
- (copied = copied || new Set()).add(propName);
684
- }
685
- else if (propName !== "children") {
686
- renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope);
687
- }
688
- }
689
- }
690
- if (copied) {
691
- props = { ...ret.el.props };
692
- for (const name of copied) {
693
- props[name] = oldProps && oldProps[name];
694
- }
695
- ret.el = new Element(tag, props, ret.el.key, ret.el.ref);
696
- }
697
- renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
698
- ret.cachedChildValues = unwrap(childValues);
699
- if (tag === Portal) {
700
- flush(renderer, ret.value);
701
- return;
702
- }
703
- return value;
704
- }
705
- function flush(renderer, root, initiator) {
706
- renderer.flush(root);
707
- if (typeof root !== "object" || root === null) {
708
- return;
709
- }
710
- const flushMap = flushMaps.get(root);
711
- if (flushMap) {
712
- if (initiator) {
713
- const flushMap1 = new Map();
714
- for (let [ctx, callbacks] of flushMap) {
715
- if (!ctxContains(initiator, ctx)) {
716
- flushMap.delete(ctx);
717
- flushMap1.set(ctx, callbacks);
718
- }
719
- }
720
- if (flushMap1.size) {
721
- flushMaps.set(root, flushMap1);
722
- }
723
- else {
724
- flushMaps.delete(root);
725
- }
726
- }
727
- else {
728
- flushMaps.delete(root);
729
- }
730
- for (const [ctx, callbacks] of flushMap) {
731
- const value = renderer.read(getValue(ctx.ret));
732
- for (const callback of callbacks) {
733
- callback(value);
734
- }
735
- }
736
- }
737
- }
738
- function unmount(renderer, host, ctx, ret) {
739
- if (typeof ret.el.tag === "function") {
740
- ctx = ret.ctx;
741
- unmountComponent(ctx);
742
- }
743
- else if (ret.el.tag === Portal) {
744
- host = ret;
745
- renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
746
- flush(renderer, host.value);
747
- }
748
- else if (ret.el.tag !== Fragment) {
749
- if (isEventTarget(ret.value)) {
750
- const records = getListenerRecords(ctx, host);
751
- for (let i = 0; i < records.length; i++) {
752
- const record = records[i];
753
- ret.value.removeEventListener(record.type, record.callback, record.options);
754
- }
755
- }
756
- renderer.dispose(ret.el.tag, ret.value, ret.el.props);
757
- host = ret;
758
- }
759
- const children = wrap(ret.children);
760
- for (let i = 0; i < children.length; i++) {
761
- const child = children[i];
762
- if (typeof child === "object") {
763
- unmount(renderer, host, ctx, child);
764
- }
765
- }
766
- }
767
- /*** CONTEXT FLAGS ***/
768
- /**
769
- * A flag which is true when the component is initialized or updated by an
770
- * ancestor component or the root render call.
771
- *
772
- * Used to determine things like whether the nearest host ancestor needs to be
773
- * rearranged.
774
- */
775
- const IsUpdating = 1 << 0;
776
- /**
777
- * A flag which is true when the component is synchronously executing.
778
- *
779
- * Used to guard against components triggering stack overflow or generator error.
780
- */
781
- const IsSyncExecuting = 1 << 1;
782
- /**
783
- * A flag which is true when the component is in a for...of loop.
784
- */
785
- const IsInForOfLoop = 1 << 2;
786
- /**
787
- * A flag which is true when the component is in a for await...of loop.
788
- */
789
- const IsInForAwaitOfLoop = 1 << 3;
790
- /**
791
- * A flag which is true when the component starts the render loop but has not
792
- * yielded yet.
793
- *
794
- * Used to make sure that components yield at least once per loop.
795
- */
796
- const NeedsToYield = 1 << 4;
797
- /**
798
- * A flag used by async generator components in conjunction with the
799
- * onAvailable callback to mark whether new props can be pulled via the context
800
- * async iterator. See the Symbol.asyncIterator method and the
801
- * resumeCtxIterator function.
802
- */
803
- const PropsAvailable = 1 << 5;
804
- /**
805
- * A flag which is set when a component errors.
806
- *
807
- * NOTE: This is mainly used to prevent some false positives in component
808
- * yields or returns undefined warnings. The reason we’re using this versus
809
- * IsUnmounted is a very troubling test (cascades sync generator parent and
810
- * sync generator child) where synchronous code causes a stack overflow error
811
- * in a non-deterministic way. Deeply disturbing stuff.
812
- */
813
- const IsErrored = 1 << 6;
814
- /**
815
- * A flag which is set when the component is unmounted. Unmounted components
816
- * are no longer in the element tree and cannot refresh or rerender.
817
- */
818
- const IsUnmounted = 1 << 7;
819
- /**
820
- * A flag which indicates that the component is a sync generator component.
821
- */
822
- const IsSyncGen = 1 << 8;
823
- /**
824
- * A flag which indicates that the component is an async generator component.
825
- */
826
- const IsAsyncGen = 1 << 9;
827
- /**
828
- * A flag which is set while schedule callbacks are called.
829
- */
830
- const IsScheduling = 1 << 10;
831
- /**
832
- * A flag which is set when a schedule callback calls refresh.
833
- */
834
- const IsSchedulingRefresh = 1 << 11;
835
- const provisionMaps = new WeakMap();
836
- const scheduleMap = new WeakMap();
837
- const cleanupMap = new WeakMap();
838
- // keys are roots
839
- const flushMaps = new WeakMap();
840
- /**
841
- * @internal
842
- * The internal class which holds context data.
843
- */
844
- class ContextImpl {
845
- constructor(renderer, root, host, parent, scope, ret) {
846
- this.f = 0;
847
- this.owner = new Context(this);
848
- this.renderer = renderer;
849
- this.root = root;
850
- this.host = host;
851
- this.parent = parent;
852
- this.scope = scope;
853
- this.ret = ret;
854
- this.iterator = undefined;
855
- this.inflightBlock = undefined;
856
- this.inflightValue = undefined;
857
- this.enqueuedBlock = undefined;
858
- this.enqueuedValue = undefined;
859
- this.onProps = undefined;
860
- this.onPropsRequested = undefined;
861
- }
862
- }
863
- const _ContextImpl = Symbol.for("crank.ContextImpl");
864
- /**
865
- * A class which is instantiated and passed to every component as its this
866
- * value. Contexts form a tree just like elements and all components in the
867
- * element tree are connected via contexts. Components can use this tree to
868
- * communicate data upwards via events and downwards via provisions.
869
- *
870
- * @template [T=*] - The expected shape of the props passed to the component,
871
- * or a component function. Used to strongly type the Context iterator methods.
872
- * @template [TResult=*] - The readable element value type. It is used in
873
- * places such as the return value of refresh and the argument passed to
874
- * schedule and cleanup callbacks.
875
- */
876
- class Context {
877
- // TODO: If we could make the constructor function take a nicer value, it
878
- // would be useful for testing purposes.
879
- constructor(impl) {
880
- this[_ContextImpl] = impl;
881
- }
882
- /**
883
- * The current props of the associated element.
884
- *
885
- * Typically, you should read props either via the first parameter of the
886
- * component or via the context iterator methods. This property is mainly for
887
- * plugins or utilities which wrap contexts.
888
- */
889
- get props() {
890
- return this[_ContextImpl].ret.el.props;
891
- }
892
- // TODO: Should we rename this???
893
- /**
894
- * The current value of the associated element.
895
- *
896
- * Typically, you should read values via refs, generator yield expressions,
897
- * or the refresh, schedule, cleanup, or flush methods. This property is
898
- * mainly for plugins or utilities which wrap contexts.
899
- */
900
- get value() {
901
- return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
902
- }
903
- *[Symbol.iterator]() {
904
- const ctx = this[_ContextImpl];
905
- try {
906
- ctx.f |= IsInForOfLoop;
907
- while (!(ctx.f & IsUnmounted)) {
908
- if (ctx.f & NeedsToYield) {
909
- throw new Error("Context iterated twice without a yield");
910
- }
911
- else {
912
- ctx.f |= NeedsToYield;
913
- }
914
- yield ctx.ret.el.props;
915
- }
916
- }
917
- finally {
918
- ctx.f &= ~IsInForOfLoop;
919
- }
920
- }
921
- async *[Symbol.asyncIterator]() {
922
- const ctx = this[_ContextImpl];
923
- if (ctx.f & IsSyncGen) {
924
- throw new Error("Use for...of in sync generator components");
925
- }
926
- try {
927
- ctx.f |= IsInForAwaitOfLoop;
928
- while (!(ctx.f & IsUnmounted)) {
929
- if (ctx.f & NeedsToYield) {
930
- throw new Error("Context iterated twice without a yield");
931
- }
932
- else {
933
- ctx.f |= NeedsToYield;
934
- }
935
- if (ctx.f & PropsAvailable) {
936
- ctx.f &= ~PropsAvailable;
937
- yield ctx.ret.el.props;
938
- }
939
- else {
940
- const props = await new Promise((resolve) => (ctx.onProps = resolve));
941
- if (ctx.f & IsUnmounted) {
942
- break;
943
- }
944
- yield props;
945
- }
946
- if (ctx.onPropsRequested) {
947
- ctx.onPropsRequested();
948
- ctx.onPropsRequested = undefined;
949
- }
950
- }
951
- }
952
- finally {
953
- ctx.f &= ~IsInForAwaitOfLoop;
954
- if (ctx.onPropsRequested) {
955
- ctx.onPropsRequested();
956
- ctx.onPropsRequested = undefined;
957
- }
958
- }
959
- }
960
- /**
961
- * Re-executes a component.
962
- *
963
- * @returns The rendered value of the component or a promise thereof if the
964
- * component or its children execute asynchronously.
965
- *
966
- * The refresh method works a little differently for async generator
967
- * components, in that it will resume the Context’s props async iterator
968
- * rather than resuming execution. This is because async generator components
969
- * are perpetually resumed independent of updates, and rely on the props
970
- * async iterator to suspend.
971
- */
972
- refresh() {
973
- const ctx = this[_ContextImpl];
974
- if (ctx.f & IsUnmounted) {
975
- console.error("Component is unmounted");
976
- return ctx.renderer.read(undefined);
977
- }
978
- else if (ctx.f & IsSyncExecuting) {
979
- console.error("Component is already executing");
980
- return this.value;
981
- }
982
- const value = enqueueComponentRun(ctx);
983
- if (isPromiseLike(value)) {
984
- return value.then((value) => ctx.renderer.read(value));
985
- }
986
- return ctx.renderer.read(value);
987
- }
988
- /**
989
- * Registers a callback which fires when the component commits. Will only
990
- * fire once per callback and update.
991
- */
992
- schedule(callback) {
993
- const ctx = this[_ContextImpl];
994
- let callbacks = scheduleMap.get(ctx);
995
- if (!callbacks) {
996
- callbacks = new Set();
997
- scheduleMap.set(ctx, callbacks);
998
- }
999
- callbacks.add(callback);
1000
- }
1001
- /**
1002
- * Registers a callback which fires when the component’s children are
1003
- * rendered into the root. Will only fire once per callback and render.
1004
- */
1005
- flush(callback) {
1006
- const ctx = this[_ContextImpl];
1007
- if (typeof ctx.root !== "object" || ctx.root === null) {
1008
- return;
1009
- }
1010
- let flushMap = flushMaps.get(ctx.root);
1011
- if (!flushMap) {
1012
- flushMap = new Map();
1013
- flushMaps.set(ctx.root, flushMap);
1014
- }
1015
- let callbacks = flushMap.get(ctx);
1016
- if (!callbacks) {
1017
- callbacks = new Set();
1018
- flushMap.set(ctx, callbacks);
1019
- }
1020
- callbacks.add(callback);
1021
- }
1022
- /**
1023
- * Registers a callback which fires when the component unmounts. Will only
1024
- * fire once per callback.
1025
- */
1026
- cleanup(callback) {
1027
- const ctx = this[_ContextImpl];
1028
- if (ctx.f & IsUnmounted) {
1029
- const value = ctx.renderer.read(getValue(ctx.ret));
1030
- callback(value);
1031
- return;
1032
- }
1033
- let callbacks = cleanupMap.get(ctx);
1034
- if (!callbacks) {
1035
- callbacks = new Set();
1036
- cleanupMap.set(ctx, callbacks);
1037
- }
1038
- callbacks.add(callback);
1039
- }
1040
- consume(key) {
1041
- for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
1042
- const provisions = provisionMaps.get(ctx);
1043
- if (provisions && provisions.has(key)) {
1044
- return provisions.get(key);
1045
- }
1046
- }
1047
- }
1048
- provide(key, value) {
1049
- const ctx = this[_ContextImpl];
1050
- let provisions = provisionMaps.get(ctx);
1051
- if (!provisions) {
1052
- provisions = new Map();
1053
- provisionMaps.set(ctx, provisions);
1054
- }
1055
- provisions.set(key, value);
1056
- }
1057
- addEventListener(type, listener, options) {
1058
- const ctx = this[_ContextImpl];
1059
- let listeners;
1060
- if (!isListenerOrListenerObject(listener)) {
1061
- return;
1062
- }
1063
- else {
1064
- const listeners1 = listenersMap.get(ctx);
1065
- if (listeners1) {
1066
- listeners = listeners1;
1067
- }
1068
- else {
1069
- listeners = [];
1070
- listenersMap.set(ctx, listeners);
1071
- }
1072
- }
1073
- options = normalizeListenerOptions(options);
1074
- let callback;
1075
- if (typeof listener === "object") {
1076
- callback = () => listener.handleEvent.apply(listener, arguments);
1077
- }
1078
- else {
1079
- callback = listener;
1080
- }
1081
- const record = { type, listener, callback, options };
1082
- if (options.once) {
1083
- record.callback = function () {
1084
- const i = listeners.indexOf(record);
1085
- if (i !== -1) {
1086
- listeners.splice(i, 1);
1087
- }
1088
- return callback.apply(this, arguments);
1089
- };
1090
- }
1091
- if (listeners.some((record1) => record.type === record1.type &&
1092
- record.listener === record1.listener &&
1093
- !record.options.capture === !record1.options.capture)) {
1094
- return;
1095
- }
1096
- listeners.push(record);
1097
- // TODO: is it possible to separate out the EventTarget delegation logic
1098
- for (const value of getChildValues(ctx.ret)) {
1099
- if (isEventTarget(value)) {
1100
- value.addEventListener(record.type, record.callback, record.options);
1101
- }
1102
- }
1103
- }
1104
- removeEventListener(type, listener, options) {
1105
- const ctx = this[_ContextImpl];
1106
- const listeners = listenersMap.get(ctx);
1107
- if (listeners == null || !isListenerOrListenerObject(listener)) {
1108
- return;
1109
- }
1110
- const options1 = normalizeListenerOptions(options);
1111
- const i = listeners.findIndex((record) => record.type === type &&
1112
- record.listener === listener &&
1113
- !record.options.capture === !options1.capture);
1114
- if (i === -1) {
1115
- return;
1116
- }
1117
- const record = listeners[i];
1118
- listeners.splice(i, 1);
1119
- // TODO: is it possible to separate out the EventTarget delegation logic
1120
- for (const value of getChildValues(ctx.ret)) {
1121
- if (isEventTarget(value)) {
1122
- value.removeEventListener(record.type, record.callback, record.options);
1123
- }
1124
- }
1125
- }
1126
- dispatchEvent(ev) {
1127
- const ctx = this[_ContextImpl];
1128
- const path = [];
1129
- for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1130
- path.push(parent);
1131
- }
1132
- // We patch the stopImmediatePropagation method because ev.cancelBubble
1133
- // only informs us if stopPropagation was called and there are no
1134
- // properties which inform us if stopImmediatePropagation was called.
1135
- let immediateCancelBubble = false;
1136
- const stopImmediatePropagation = ev.stopImmediatePropagation;
1137
- setEventProperty(ev, "stopImmediatePropagation", () => {
1138
- immediateCancelBubble = true;
1139
- return stopImmediatePropagation.call(ev);
1140
- });
1141
- setEventProperty(ev, "target", ctx.owner);
1142
- // The only possible errors in this block are errors thrown by callbacks,
1143
- // and dispatchEvent will only log these errors rather than throwing
1144
- // them. Therefore, we place all code in a try block, log errors in the
1145
- // catch block, and use an unsafe return statement in the finally block.
1146
- //
1147
- // Each early return within the try block returns true because while the
1148
- // return value is overridden in the finally block, TypeScript
1149
- // (justifiably) does not recognize the unsafe return statement.
1150
- try {
1151
- setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1152
- for (let i = path.length - 1; i >= 0; i--) {
1153
- const target = path[i];
1154
- const listeners = listenersMap.get(target);
1155
- if (listeners) {
1156
- setEventProperty(ev, "currentTarget", target.owner);
1157
- for (const record of listeners) {
1158
- if (record.type === ev.type && record.options.capture) {
1159
- try {
1160
- record.callback.call(target.owner, ev);
1161
- }
1162
- catch (err) {
1163
- console.error(err);
1164
- }
1165
- if (immediateCancelBubble) {
1166
- return true;
1167
- }
1168
- }
1169
- }
1170
- }
1171
- if (ev.cancelBubble) {
1172
- return true;
1173
- }
1174
- }
1175
- {
1176
- setEventProperty(ev, "eventPhase", AT_TARGET);
1177
- setEventProperty(ev, "currentTarget", ctx.owner);
1178
- const propCallback = ctx.ret.el.props["on" + ev.type];
1179
- if (propCallback != null) {
1180
- propCallback(ev);
1181
- if (immediateCancelBubble || ev.cancelBubble) {
1182
- return true;
1183
- }
1184
- }
1185
- const listeners = listenersMap.get(ctx);
1186
- if (listeners) {
1187
- for (const record of listeners) {
1188
- if (record.type === ev.type) {
1189
- try {
1190
- record.callback.call(ctx.owner, ev);
1191
- }
1192
- catch (err) {
1193
- console.error(err);
1194
- }
1195
- if (immediateCancelBubble) {
1196
- return true;
1197
- }
1198
- }
1199
- }
1200
- if (ev.cancelBubble) {
1201
- return true;
1202
- }
1203
- }
1204
- }
1205
- if (ev.bubbles) {
1206
- setEventProperty(ev, "eventPhase", BUBBLING_PHASE);
1207
- for (let i = 0; i < path.length; i++) {
1208
- const target = path[i];
1209
- const listeners = listenersMap.get(target);
1210
- if (listeners) {
1211
- setEventProperty(ev, "currentTarget", target.owner);
1212
- for (const record of listeners) {
1213
- if (record.type === ev.type && !record.options.capture) {
1214
- try {
1215
- record.callback.call(target.owner, ev);
1216
- }
1217
- catch (err) {
1218
- console.error(err);
1219
- }
1220
- if (immediateCancelBubble) {
1221
- return true;
1222
- }
1223
- }
1224
- }
1225
- }
1226
- if (ev.cancelBubble) {
1227
- return true;
1228
- }
1229
- }
1230
- }
1231
- }
1232
- finally {
1233
- setEventProperty(ev, "eventPhase", NONE);
1234
- setEventProperty(ev, "currentTarget", null);
1235
- // eslint-disable-next-line no-unsafe-finally
1236
- return !ev.defaultPrevented;
1237
- }
1238
- }
1239
- }
1240
- /*** PRIVATE CONTEXT FUNCTIONS ***/
1241
- function ctxContains(parent, child) {
1242
- for (let current = child; current !== undefined; current = current.parent) {
1243
- if (current === parent) {
1244
- return true;
1245
- }
1246
- }
1247
- return false;
1248
- }
1249
- function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) {
1250
- let ctx;
1251
- if (oldProps) {
1252
- ctx = ret.ctx;
1253
- if (ctx.f & IsSyncExecuting) {
1254
- console.error("Component is already executing");
1255
- return ret.cachedChildValues;
1256
- }
1257
- }
1258
- else {
1259
- ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1260
- }
1261
- ctx.f |= IsUpdating;
1262
- return enqueueComponentRun(ctx, hydrationData);
1263
- }
1264
- function updateComponentChildren(ctx, children, hydrationData) {
1265
- if (ctx.f & IsUnmounted) {
1266
- return;
1267
- }
1268
- else if (ctx.f & IsErrored) {
1269
- // This branch is necessary for some race conditions where this function is
1270
- // called after iterator.throw() in async generator components.
1271
- return;
1272
- }
1273
- else if (children === undefined) {
1274
- console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1275
- }
1276
- let childValues;
1277
- try {
1278
- // TODO: WAT
1279
- // We set the isExecuting flag in case a child component dispatches an event
1280
- // which bubbles to this component and causes a synchronous refresh().
1281
- ctx.f |= IsSyncExecuting;
1282
- childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData);
1283
- }
1284
- finally {
1285
- ctx.f &= ~IsSyncExecuting;
1286
- }
1287
- if (isPromiseLike(childValues)) {
1288
- ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1289
- return ctx.ret.inflightValue;
1290
- }
1291
- return commitComponent(ctx, childValues);
1292
- }
1293
- function commitComponent(ctx, values) {
1294
- if (ctx.f & IsUnmounted) {
1295
- return;
1296
- }
1297
- const listeners = listenersMap.get(ctx);
1298
- if (listeners && listeners.length) {
1299
- for (let i = 0; i < values.length; i++) {
1300
- const value = values[i];
1301
- if (isEventTarget(value)) {
1302
- for (let j = 0; j < listeners.length; j++) {
1303
- const record = listeners[j];
1304
- value.addEventListener(record.type, record.callback, record.options);
1305
- }
1306
- }
1307
- }
1308
- }
1309
- const oldValues = wrap(ctx.ret.cachedChildValues);
1310
- let value = (ctx.ret.cachedChildValues = unwrap(values));
1311
- if (ctx.f & IsScheduling) {
1312
- ctx.f |= IsSchedulingRefresh;
1313
- }
1314
- else if (!(ctx.f & IsUpdating)) {
1315
- // If we’re not updating the component, which happens when components are
1316
- // refreshed, or when async generator components iterate, we have to do a
1317
- // little bit housekeeping when a component’s child values have changed.
1318
- if (!arrayEqual(oldValues, values)) {
1319
- const records = getListenerRecords(ctx.parent, ctx.host);
1320
- if (records.length) {
1321
- for (let i = 0; i < values.length; i++) {
1322
- const value = values[i];
1323
- if (isEventTarget(value)) {
1324
- for (let j = 0; j < records.length; j++) {
1325
- const record = records[j];
1326
- value.addEventListener(record.type, record.callback, record.options);
1327
- }
1328
- }
1329
- }
1330
- }
1331
- // rearranging the nearest ancestor host element
1332
- const host = ctx.host;
1333
- const oldHostValues = wrap(host.cachedChildValues);
1334
- invalidate(ctx, host);
1335
- const hostValues = getChildValues(host);
1336
- ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
1337
- // props and oldProps are the same because the host isn’t updated.
1338
- host.el.props, oldHostValues);
1339
- }
1340
- flush(ctx.renderer, ctx.root, ctx);
1341
- }
1342
- const callbacks = scheduleMap.get(ctx);
1343
- if (callbacks) {
1344
- scheduleMap.delete(ctx);
1345
- ctx.f |= IsScheduling;
1346
- const value1 = ctx.renderer.read(value);
1347
- for (const callback of callbacks) {
1348
- callback(value1);
1349
- }
1350
- ctx.f &= ~IsScheduling;
1351
- // Handles an edge case where refresh() is called during a schedule().
1352
- if (ctx.f & IsSchedulingRefresh) {
1353
- ctx.f &= ~IsSchedulingRefresh;
1354
- value = getValue(ctx.ret);
1355
- }
1356
- }
1357
- ctx.f &= ~IsUpdating;
1358
- return value;
1359
- }
1360
- function invalidate(ctx, host) {
1361
- for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1362
- parent.ret.cachedChildValues = undefined;
1363
- }
1364
- host.cachedChildValues = undefined;
1365
- }
1366
- function arrayEqual(arr1, arr2) {
1367
- if (arr1.length !== arr2.length) {
1368
- return false;
1369
- }
1370
- for (let i = 0; i < arr1.length; i++) {
1371
- const value1 = arr1[i];
1372
- const value2 = arr2[i];
1373
- if (value1 !== value2) {
1374
- return false;
1375
- }
1376
- }
1377
- return true;
1378
- }
1379
- /** Enqueues and executes the component associated with the context. */
1380
- function enqueueComponentRun(ctx, hydrationData) {
1381
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1382
- if (hydrationData !== undefined) {
1383
- throw new Error("Hydration error");
1384
- }
1385
- // This branch will run for non-initial renders of async generator
1386
- // components when they are not in for...of loops. When in a for...of loop,
1387
- // async generator components will behave normally.
1388
- //
1389
- // Async gen componennts can be in one of three states:
1390
- //
1391
- // 1. propsAvailable flag is true: "available"
1392
- //
1393
- // The component is suspended somewhere in the loop. When the component
1394
- // reaches the bottom of the loop, it will run again with the next props.
1395
- //
1396
- // 2. onAvailable callback is defined: "suspended"
1397
- //
1398
- // The component has suspended at the bottom of the loop and is waiting
1399
- // for new props.
1400
- //
1401
- // 3. neither 1 or 2: "Running"
1402
- //
1403
- // The component is suspended somewhere in the loop. When the component
1404
- // reaches the bottom of the loop, it will suspend.
1405
- //
1406
- // Components will never be both available and suspended at
1407
- // the same time.
1408
- //
1409
- // If the component is at the loop bottom, this means that the next value
1410
- // produced by the component will have the most up to date props, so we can
1411
- // simply return the current inflight value. Otherwise, we have to wait for
1412
- // the bottom of the loop to be reached before returning the inflight
1413
- // value.
1414
- const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
1415
- resumePropsIterator(ctx);
1416
- if (isAtLoopbottom) {
1417
- if (ctx.inflightBlock == null) {
1418
- ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1419
- }
1420
- return ctx.inflightBlock.then(() => {
1421
- ctx.inflightBlock = undefined;
1422
- return ctx.inflightValue;
1423
- });
1424
- }
1425
- return ctx.inflightValue;
1426
- }
1427
- else if (!ctx.inflightBlock) {
1428
- try {
1429
- const [block, value] = runComponent(ctx, hydrationData);
1430
- if (block) {
1431
- ctx.inflightBlock = block
1432
- // TODO: there is some fuckery going on here related to async
1433
- // generator components resuming when they’re meant to be returned.
1434
- .then((v) => v)
1435
- .finally(() => advanceComponent(ctx));
1436
- // stepComponent will only return a block if the value is asynchronous
1437
- ctx.inflightValue = value;
1438
- }
1439
- return value;
1440
- }
1441
- catch (err) {
1442
- if (!(ctx.f & IsUpdating)) {
1443
- if (!ctx.parent) {
1444
- throw err;
1445
- }
1446
- return propagateError(ctx.parent, err);
1447
- }
1448
- throw err;
1449
- }
1450
- }
1451
- else if (!ctx.enqueuedBlock) {
1452
- if (hydrationData !== undefined) {
1453
- throw new Error("Hydration error");
1454
- }
1455
- // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1456
- // the Promise constructor call here.
1457
- let resolveEnqueuedBlock;
1458
- ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1459
- ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1460
- try {
1461
- const [block, value] = runComponent(ctx);
1462
- if (block) {
1463
- resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1464
- }
1465
- return value;
1466
- }
1467
- catch (err) {
1468
- if (!(ctx.f & IsUpdating)) {
1469
- if (!ctx.parent) {
1470
- throw err;
1471
- }
1472
- return propagateError(ctx.parent, err);
1473
- }
1474
- throw err;
1475
- }
1476
- });
1477
- }
1478
- return ctx.enqueuedValue;
1479
- }
1480
- /** Called when the inflight block promise settles. */
1481
- function advanceComponent(ctx) {
1482
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1483
- return;
1484
- }
1485
- ctx.inflightBlock = ctx.enqueuedBlock;
1486
- ctx.inflightValue = ctx.enqueuedValue;
1487
- ctx.enqueuedBlock = undefined;
1488
- ctx.enqueuedValue = undefined;
1489
- }
1490
- /**
1491
- * This function is responsible for executing the component and handling all
1492
- * the different component types. We cannot identify whether a component is a
1493
- * generator or async without calling it and inspecting the return value.
1494
- *
1495
- * @returns {[block, value]} A tuple where
1496
- * block - A possible promise which represents the duration during which the
1497
- * component is blocked from updating.
1498
- * value - A possible promise resolving to the rendered value of children.
1499
- *
1500
- * Each component type will block according to the type of the component.
1501
- * - Sync function components never block and will transparently pass updates
1502
- * to children.
1503
- * - Async function components and async generator components block while
1504
- * executing itself, but will not block for async children.
1505
- * - Sync generator components block while any children are executing, because
1506
- * they are expected to only resume when they’ve actually rendered.
1507
- */
1508
- function runComponent(ctx, hydrationData) {
1509
- const ret = ctx.ret;
1510
- const initial = !ctx.iterator;
1511
- if (initial) {
1512
- resumePropsIterator(ctx);
1513
- ctx.f |= IsSyncExecuting;
1514
- clearEventListeners(ctx);
1515
- let result;
1516
- try {
1517
- result = ret.el.tag.call(ctx.owner, ret.el.props);
1518
- }
1519
- catch (err) {
1520
- ctx.f |= IsErrored;
1521
- throw err;
1522
- }
1523
- finally {
1524
- ctx.f &= ~IsSyncExecuting;
1525
- }
1526
- if (isIteratorLike(result)) {
1527
- ctx.iterator = result;
1528
- }
1529
- else if (isPromiseLike(result)) {
1530
- // async function component
1531
- const result1 = result instanceof Promise ? result : Promise.resolve(result);
1532
- const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => {
1533
- ctx.f |= IsErrored;
1534
- throw err;
1535
- });
1536
- return [result1.catch(NOOP), value];
1537
- }
1538
- else {
1539
- // sync function component
1540
- return [
1541
- undefined,
1542
- updateComponentChildren(ctx, result, hydrationData),
1543
- ];
1544
- }
1545
- }
1546
- else if (hydrationData !== undefined) {
1547
- throw new Error("Hydration error");
1548
- }
1549
- let iteration;
1550
- if (initial) {
1551
- try {
1552
- ctx.f |= IsSyncExecuting;
1553
- iteration = ctx.iterator.next();
1554
- }
1555
- catch (err) {
1556
- ctx.f |= IsErrored;
1557
- throw err;
1558
- }
1559
- finally {
1560
- ctx.f &= ~IsSyncExecuting;
1561
- }
1562
- if (isPromiseLike(iteration)) {
1563
- ctx.f |= IsAsyncGen;
1564
- }
1565
- else {
1566
- ctx.f |= IsSyncGen;
1567
- }
1568
- }
1569
- if (ctx.f & IsSyncGen) {
1570
- ctx.f &= ~NeedsToYield;
1571
- // sync generator component
1572
- if (!initial) {
1573
- try {
1574
- ctx.f |= IsSyncExecuting;
1575
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1576
- }
1577
- catch (err) {
1578
- ctx.f |= IsErrored;
1579
- throw err;
1580
- }
1581
- finally {
1582
- ctx.f &= ~IsSyncExecuting;
1583
- }
1584
- }
1585
- if (isPromiseLike(iteration)) {
1586
- throw new Error("Mixed generator component");
1587
- }
1588
- if (iteration.done) {
1589
- ctx.f &= ~IsSyncGen;
1590
- ctx.iterator = undefined;
1591
- }
1592
- let value;
1593
- try {
1594
- value = updateComponentChildren(ctx,
1595
- // Children can be void so we eliminate that here
1596
- iteration.value, hydrationData);
1597
- if (isPromiseLike(value)) {
1598
- value = value.catch((err) => handleChildError(ctx, err));
1599
- }
1600
- }
1601
- catch (err) {
1602
- value = handleChildError(ctx, err);
1603
- }
1604
- const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1605
- return [block, value];
1606
- }
1607
- else if (ctx.f & IsInForOfLoop) {
1608
- // TODO: does this need to be done async?
1609
- ctx.f &= ~NeedsToYield;
1610
- // we are in a for...of loop for async generator
1611
- if (!initial) {
1612
- try {
1613
- ctx.f |= IsSyncExecuting;
1614
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1615
- }
1616
- catch (err) {
1617
- ctx.f |= IsErrored;
1618
- throw err;
1619
- }
1620
- finally {
1621
- ctx.f &= ~IsSyncExecuting;
1622
- }
1623
- }
1624
- if (!isPromiseLike(iteration)) {
1625
- throw new Error("Mixed generator component");
1626
- }
1627
- const block = iteration.catch(NOOP);
1628
- const value = iteration.then((iteration) => {
1629
- let value;
1630
- if (!(ctx.f & IsInForOfLoop)) {
1631
- runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
1632
- }
1633
- try {
1634
- value = updateComponentChildren(ctx,
1635
- // Children can be void so we eliminate that here
1636
- iteration.value, hydrationData);
1637
- if (isPromiseLike(value)) {
1638
- value = value.catch((err) => handleChildError(ctx, err));
1639
- }
1640
- }
1641
- catch (err) {
1642
- value = handleChildError(ctx, err);
1643
- }
1644
- return value;
1645
- }, (err) => {
1646
- ctx.f |= IsErrored;
1647
- throw err;
1648
- });
1649
- return [block, value];
1650
- }
1651
- else {
1652
- runAsyncGenComponent(ctx, iteration, hydrationData);
1653
- // async generator component
1654
- return [ctx.inflightBlock, ctx.inflightValue];
1655
- }
1656
- }
1657
- async function runAsyncGenComponent(ctx, iterationP, hydrationData) {
1658
- let done = false;
1659
- try {
1660
- while (!done) {
1661
- if (ctx.f & IsInForOfLoop) {
1662
- break;
1663
- }
1664
- // inflightValue must be set synchronously.
1665
- let onValue;
1666
- ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1667
- if (ctx.f & IsUpdating) {
1668
- // We should not swallow unhandled promise rejections if the component is
1669
- // updating independently.
1670
- // TODO: Does this handle this.refresh() calls?
1671
- ctx.inflightValue.catch(NOOP);
1672
- }
1673
- let iteration;
1674
- try {
1675
- iteration = await iterationP;
1676
- }
1677
- catch (err) {
1678
- done = true;
1679
- ctx.f |= IsErrored;
1680
- onValue(Promise.reject(err));
1681
- break;
1682
- }
1683
- finally {
1684
- ctx.f &= ~NeedsToYield;
1685
- if (!(ctx.f & IsInForAwaitOfLoop)) {
1686
- ctx.f &= ~PropsAvailable;
1687
- }
1688
- }
1689
- done = !!iteration.done;
1690
- let value;
1691
- try {
1692
- value = updateComponentChildren(ctx, iteration.value, hydrationData);
1693
- hydrationData = undefined;
1694
- if (isPromiseLike(value)) {
1695
- value = value.catch((err) => handleChildError(ctx, err));
1696
- }
1697
- }
1698
- catch (err) {
1699
- // Do we need to catch potential errors here in the case of unhandled
1700
- // promise rejections?
1701
- value = handleChildError(ctx, err);
1702
- }
1703
- finally {
1704
- onValue(value);
1705
- }
1706
- // TODO: this can be done more elegantly
1707
- let oldValue;
1708
- if (ctx.ret.inflightValue) {
1709
- // The value passed back into the generator as the argument to the next
1710
- // method is a promise if an async generator component has async
1711
- // children. Sync generator components only resume when their children
1712
- // have fulfilled so the element’s inflight child values will never be
1713
- // defined.
1714
- oldValue = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value));
1715
- oldValue.catch((err) => {
1716
- if (ctx.f & IsUpdating) {
1717
- return;
1718
- }
1719
- if (!ctx.parent) {
1720
- throw err;
1721
- }
1722
- return propagateError(ctx.parent, err);
1723
- });
1724
- }
1725
- else {
1726
- oldValue = ctx.renderer.read(getValue(ctx.ret));
1727
- }
1728
- if (ctx.f & IsUnmounted) {
1729
- if (ctx.f & IsInForAwaitOfLoop) {
1730
- try {
1731
- ctx.f |= IsSyncExecuting;
1732
- iterationP = ctx.iterator.next(oldValue);
1733
- }
1734
- finally {
1735
- ctx.f &= ~IsSyncExecuting;
1736
- }
1737
- }
1738
- else {
1739
- returnComponent(ctx);
1740
- break;
1741
- }
1742
- }
1743
- else if (!done && !(ctx.f & IsInForOfLoop)) {
1744
- try {
1745
- ctx.f |= IsSyncExecuting;
1746
- iterationP = ctx.iterator.next(oldValue);
1747
- }
1748
- finally {
1749
- ctx.f &= ~IsSyncExecuting;
1750
- }
1751
- }
1752
- }
1753
- }
1754
- finally {
1755
- if (done) {
1756
- ctx.f &= ~IsAsyncGen;
1757
- ctx.iterator = undefined;
1758
- }
1759
- }
1760
- }
1761
- /**
1762
- * Called to resume the props async iterator for async generator components.
1763
- */
1764
- function resumePropsIterator(ctx) {
1765
- if (ctx.onProps) {
1766
- ctx.onProps(ctx.ret.el.props);
1767
- ctx.onProps = undefined;
1768
- ctx.f &= ~PropsAvailable;
1769
- }
1770
- else {
1771
- ctx.f |= PropsAvailable;
1772
- }
1773
- }
1774
- // TODO: async unmounting
1775
- function unmountComponent(ctx) {
1776
- if (ctx.f & IsUnmounted) {
1777
- return;
1778
- }
1779
- clearEventListeners(ctx);
1780
- const callbacks = cleanupMap.get(ctx);
1781
- if (callbacks) {
1782
- cleanupMap.delete(ctx);
1783
- const value = ctx.renderer.read(getValue(ctx.ret));
1784
- for (const callback of callbacks) {
1785
- callback(value);
1786
- }
1787
- }
1788
- ctx.f |= IsUnmounted;
1789
- if (ctx.iterator) {
1790
- if (ctx.f & IsSyncGen) {
1791
- let value;
1792
- if (ctx.f & IsInForOfLoop) {
1793
- value = enqueueComponentRun(ctx);
1794
- }
1795
- if (isPromiseLike(value)) {
1796
- value.then(() => {
1797
- if (ctx.f & IsInForOfLoop) {
1798
- unmountComponent(ctx);
1799
- }
1800
- else {
1801
- returnComponent(ctx);
1802
- }
1803
- }, (err) => {
1804
- if (!ctx.parent) {
1805
- throw err;
1806
- }
1807
- return propagateError(ctx.parent, err);
1808
- });
1809
- }
1810
- else {
1811
- if (ctx.f & IsInForOfLoop) {
1812
- unmountComponent(ctx);
1813
- }
1814
- else {
1815
- returnComponent(ctx);
1816
- }
1817
- }
1818
- }
1819
- else if (ctx.f & IsAsyncGen) {
1820
- if (ctx.f & IsInForOfLoop) {
1821
- const value = enqueueComponentRun(ctx);
1822
- value.then(() => {
1823
- if (ctx.f & IsInForOfLoop) {
1824
- unmountComponent(ctx);
1825
- }
1826
- else {
1827
- returnComponent(ctx);
1828
- }
1829
- }, (err) => {
1830
- if (!ctx.parent) {
1831
- throw err;
1832
- }
1833
- return propagateError(ctx.parent, err);
1834
- });
1835
- }
1836
- else {
1837
- // The logic for unmounting async generator components is in the
1838
- // runAsyncGenComponent function.
1839
- resumePropsIterator(ctx);
1840
- }
1841
- }
1842
- }
1843
- }
1844
- function returnComponent(ctx) {
1845
- resumePropsIterator(ctx);
1846
- if (ctx.iterator && typeof ctx.iterator.return === "function") {
1847
- try {
1848
- ctx.f |= IsSyncExecuting;
1849
- const iteration = ctx.iterator.return();
1850
- if (isPromiseLike(iteration)) {
1851
- iteration.catch((err) => {
1852
- if (!ctx.parent) {
1853
- throw err;
1854
- }
1855
- return propagateError(ctx.parent, err);
1856
- });
1857
- }
1858
- }
1859
- finally {
1860
- ctx.f &= ~IsSyncExecuting;
1861
- }
1862
- }
1863
- }
1864
- /*** EVENT TARGET UTILITIES ***/
1865
- // EVENT PHASE CONSTANTS
1866
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
1867
- const NONE = 0;
1868
- const CAPTURING_PHASE = 1;
1869
- const AT_TARGET = 2;
1870
- const BUBBLING_PHASE = 3;
1871
- const listenersMap = new WeakMap();
1872
- function isListenerOrListenerObject(value) {
1873
- return (typeof value === "function" ||
1874
- (value !== null &&
1875
- typeof value === "object" &&
1876
- typeof value.handleEvent === "function"));
1877
- }
1878
- function normalizeListenerOptions(options) {
1879
- if (typeof options === "boolean") {
1880
- return { capture: options };
1881
- }
1882
- else if (options == null) {
1883
- return {};
1884
- }
1885
- return options;
1886
- }
1887
- function isEventTarget(value) {
1888
- return (value != null &&
1889
- typeof value.addEventListener === "function" &&
1890
- typeof value.removeEventListener === "function" &&
1891
- typeof value.dispatchEvent === "function");
1892
- }
1893
- function setEventProperty(ev, key, value) {
1894
- Object.defineProperty(ev, key, { value, writable: false, configurable: true });
1895
- }
1896
- // TODO: Maybe we can pass in the current context directly, rather than
1897
- // starting from the parent?
1898
- /**
1899
- * A function to reconstruct an array of every listener given a context and a
1900
- * host element.
1901
- *
1902
- * This function exploits the fact that contexts retain their nearest ancestor
1903
- * host element. We can determine all the contexts which are directly listening
1904
- * to an element by traversing up the context tree and checking that the host
1905
- * element passed in matches the parent context’s host element.
1906
- */
1907
- function getListenerRecords(ctx, ret) {
1908
- let listeners = [];
1909
- while (ctx !== undefined && ctx.host === ret) {
1910
- const listeners1 = listenersMap.get(ctx);
1911
- if (listeners1) {
1912
- listeners = listeners.concat(listeners1);
1913
- }
1914
- ctx = ctx.parent;
1915
- }
1916
- return listeners;
1917
- }
1918
- function clearEventListeners(ctx) {
1919
- const listeners = listenersMap.get(ctx);
1920
- if (listeners && listeners.length) {
1921
- for (const value of getChildValues(ctx.ret)) {
1922
- if (isEventTarget(value)) {
1923
- for (const record of listeners) {
1924
- value.removeEventListener(record.type, record.callback, record.options);
1925
- }
1926
- }
1927
- }
1928
- listeners.length = 0;
1929
- }
1930
- }
1931
- /*** ERROR HANDLING UTILITIES ***/
1932
- function handleChildError(ctx, err) {
1933
- if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
1934
- throw err;
1935
- }
1936
- resumePropsIterator(ctx);
1937
- let iteration;
1938
- try {
1939
- ctx.f |= IsSyncExecuting;
1940
- iteration = ctx.iterator.throw(err);
1941
- }
1942
- catch (err) {
1943
- ctx.f |= IsErrored;
1944
- throw err;
1945
- }
1946
- finally {
1947
- ctx.f &= ~IsSyncExecuting;
1948
- }
1949
- if (isPromiseLike(iteration)) {
1950
- return iteration.then((iteration) => {
1951
- if (iteration.done) {
1952
- ctx.f &= ~IsAsyncGen;
1953
- ctx.iterator = undefined;
1954
- }
1955
- return updateComponentChildren(ctx, iteration.value);
1956
- }, (err) => {
1957
- ctx.f |= IsErrored;
1958
- throw err;
1959
- });
1960
- }
1961
- if (iteration.done) {
1962
- ctx.f &= ~IsSyncGen;
1963
- ctx.f &= ~IsAsyncGen;
1964
- ctx.iterator = undefined;
1965
- }
1966
- return updateComponentChildren(ctx, iteration.value);
1967
- }
1968
- function propagateError(ctx, err) {
1969
- let result;
1970
- try {
1971
- result = handleChildError(ctx, err);
1972
- }
1973
- catch (err) {
1974
- if (!ctx.parent) {
1975
- throw err;
1976
- }
1977
- return propagateError(ctx.parent, err);
1978
- }
1979
- if (isPromiseLike(result)) {
1980
- return result.catch((err) => {
1981
- if (!ctx.parent) {
1982
- throw err;
1983
- }
1984
- return propagateError(ctx.parent, err);
1985
- });
1986
- }
1987
- return result;
7
+ const NOOP = () => { };
8
+ const IDENTITY = (value) => value;
9
+ function wrap(value) {
10
+ return value === undefined ? [] : Array.isArray(value) ? value : [value];
11
+ }
12
+ function unwrap(arr) {
13
+ return arr.length === 0 ? undefined : arr.length === 1 ? arr[0] : arr;
14
+ }
15
+ /**
16
+ * Ensures a value is an array.
17
+ *
18
+ * This function does the same thing as wrap() above except it handles nulls
19
+ * and iterables, so it is appropriate for wrapping user-provided element
20
+ * children.
21
+ */
22
+ function arrayify(value) {
23
+ return value == null
24
+ ? []
25
+ : Array.isArray(value)
26
+ ? value
27
+ : typeof value === "string" ||
28
+ typeof value[Symbol.iterator] !== "function"
29
+ ? [value]
30
+ : [...value];
31
+ }
32
+ function isIteratorLike(value) {
33
+ return value != null && typeof value.next === "function";
34
+ }
35
+ function isPromiseLike(value) {
36
+ return value != null && typeof value.then === "function";
37
+ }
38
+ /***
39
+ * SPECIAL TAGS
40
+ *
41
+ * Crank provides a couple tags which have special meaning for the renderer.
42
+ ***/
43
+ /**
44
+ * A special tag for grouping multiple children within the same parent.
45
+ *
46
+ * All non-string iterables which appear in the element tree are implicitly
47
+ * wrapped in a fragment element.
48
+ *
49
+ * This tag is just the empty string, and you can use the empty string in
50
+ * createElement calls or transpiler options directly to avoid having to
51
+ * reference this export.
52
+ */
53
+ const Fragment = "";
54
+ // TODO: We assert the following symbol tags as any because TypeScript support
55
+ // for symbol tags in JSX doesn’t exist yet.
56
+ // https://github.com/microsoft/TypeScript/issues/38367
57
+ /**
58
+ * A special tag for rendering into a new root node via a root prop.
59
+ *
60
+ * This tag is useful for creating element trees with multiple roots, for
61
+ * things like modals or tooltips.
62
+ *
63
+ * Renderer.prototype.render() will implicitly wrap top-level element trees in
64
+ * a Portal element.
65
+ */
66
+ const Portal = Symbol.for("crank.Portal");
67
+ /**
68
+ * A special tag which preserves whatever was previously rendered in the
69
+ * element’s position.
70
+ *
71
+ * Copy elements are useful for when you want to prevent a subtree from
72
+ * rerendering as a performance optimization. Copy elements can also be keyed,
73
+ * in which case the previously rendered keyed element will be copied.
74
+ */
75
+ const Copy = Symbol.for("crank.Copy");
76
+ /**
77
+ * A special tag for injecting raw nodes or strings via a value prop.
78
+ *
79
+ * Renderer.prototype.raw() is called with the value prop.
80
+ */
81
+ const Raw = Symbol.for("crank.Raw");
82
+ const ElementSymbol = Symbol.for("crank.Element");
83
+ /**
84
+ * Elements are the basic building blocks of Crank applications. They are
85
+ * JavaScript objects which are interpreted by special classes called renderers
86
+ * to produce and manage stateful nodes.
87
+ *
88
+ * @template {Tag} [TTag=Tag] - The type of the tag of the element.
89
+ *
90
+ * @example
91
+ * // specific element types
92
+ * let div: Element<"div">;
93
+ * let portal: Element<Portal>;
94
+ * let myEl: Element<MyComponent>;
95
+ *
96
+ * // general element types
97
+ * let host: Element<string | symbol>;
98
+ * let component: Element<Component>;
99
+ *
100
+ * Typically, you use a helper function like createElement to create elements
101
+ * rather than instatiating this class directly.
102
+ */
103
+ class Element {
104
+ constructor(tag, props) {
105
+ this.tag = tag;
106
+ this.props = props;
107
+ }
108
+ get key() {
109
+ return this.props.key;
110
+ }
111
+ get ref() {
112
+ return this.props.ref;
113
+ }
114
+ get copy() {
115
+ return !!this.props.copy;
116
+ }
117
+ }
118
+ // See Element interface
119
+ Element.prototype.$$typeof = ElementSymbol;
120
+ function isElement(value) {
121
+ return value != null && value.$$typeof === ElementSymbol;
122
+ }
123
+ const DEPRECATED_PROP_PREFIXES = ["crank-", "c-", "$"];
124
+ const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static"];
125
+ const SPECIAL_PROPS = new Set(["children", "key", "ref", "copy"]);
126
+ for (const propPrefix of DEPRECATED_PROP_PREFIXES) {
127
+ for (const propBase of DEPRECATED_SPECIAL_PROP_BASES) {
128
+ SPECIAL_PROPS.add(propPrefix + propBase);
129
+ }
130
+ }
131
+ /**
132
+ * Creates an element with the specified tag, props and children.
133
+ *
134
+ * This function is usually used as a transpilation target for JSX transpilers,
135
+ * but it can also be called directly. It additionally extracts special props so
136
+ * they aren’t accessible to renderer methods or components, and assigns the
137
+ * children prop according to any additional arguments passed to the function.
138
+ */
139
+ function createElement(tag, props, ...children) {
140
+ if (props == null) {
141
+ props = {};
142
+ }
143
+ for (let i = 0; i < DEPRECATED_PROP_PREFIXES.length; i++) {
144
+ const propPrefix = DEPRECATED_PROP_PREFIXES[i];
145
+ for (let j = 0; j < DEPRECATED_SPECIAL_PROP_BASES.length; j++) {
146
+ const propBase = DEPRECATED_SPECIAL_PROP_BASES[j];
147
+ const deprecatedPropName = propPrefix + propBase;
148
+ const targetPropBase = propBase === "static" ? "copy" : propBase;
149
+ if (deprecatedPropName in props) {
150
+ console.warn(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`);
151
+ props[targetPropBase] = props[deprecatedPropName];
152
+ }
153
+ }
154
+ }
155
+ if (children.length > 1) {
156
+ props.children = children;
157
+ }
158
+ else if (children.length === 1) {
159
+ props.children = children[0];
160
+ }
161
+ return new Element(tag, props);
162
+ }
163
+ /** Clones a given element, shallowly copying the props object. */
164
+ function cloneElement(el) {
165
+ if (!isElement(el)) {
166
+ throw new TypeError("Cannot clone non-element");
167
+ }
168
+ return new Element(el.tag, { ...el.props });
169
+ }
170
+ function narrow(value) {
171
+ if (typeof value === "boolean" || value == null) {
172
+ return undefined;
173
+ }
174
+ else if (typeof value === "string" || isElement(value)) {
175
+ return value;
176
+ }
177
+ else if (typeof value[Symbol.iterator] === "function") {
178
+ return createElement(Fragment, null, value);
179
+ }
180
+ return value.toString();
181
+ }
182
+ /**
183
+ * Takes an array of element values and normalizes the output as an array of
184
+ * nodes and strings.
185
+ *
186
+ * @returns Normalized array of nodes and/or strings.
187
+ *
188
+ * Normalize will flatten only one level of nested arrays, because it is
189
+ * designed to be called once at each level of the tree. It will also
190
+ * concatenate adjacent strings and remove all undefined values.
191
+ */
192
+ function normalize(values) {
193
+ const result = [];
194
+ let buffer;
195
+ for (let i = 0; i < values.length; i++) {
196
+ const value = values[i];
197
+ if (!value) ;
198
+ else if (typeof value === "string") {
199
+ buffer = (buffer || "") + value;
200
+ }
201
+ else if (!Array.isArray(value)) {
202
+ if (buffer) {
203
+ result.push(buffer);
204
+ buffer = undefined;
205
+ }
206
+ result.push(value);
207
+ }
208
+ else {
209
+ // We could use recursion here but it’s just easier to do it inline.
210
+ for (let j = 0; j < value.length; j++) {
211
+ const value1 = value[j];
212
+ if (!value1) ;
213
+ else if (typeof value1 === "string") {
214
+ buffer = (buffer || "") + value1;
215
+ }
216
+ else {
217
+ if (buffer) {
218
+ result.push(buffer);
219
+ buffer = undefined;
220
+ }
221
+ result.push(value1);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ if (buffer) {
227
+ result.push(buffer);
228
+ }
229
+ return result;
230
+ }
231
+ /**
232
+ * @internal
233
+ * The internal nodes which are cached and diffed against new elements when
234
+ * rendering element trees.
235
+ */
236
+ class Retainer {
237
+ constructor(el) {
238
+ this.el = el;
239
+ this.ctx = undefined;
240
+ this.children = undefined;
241
+ this.value = undefined;
242
+ this.cachedChildValues = undefined;
243
+ this.fallbackValue = undefined;
244
+ this.inflightValue = undefined;
245
+ this.onNextValues = undefined;
246
+ }
247
+ }
248
+ /**
249
+ * Finds the value of the element according to its type.
250
+ *
251
+ * @returns The value of the element.
252
+ */
253
+ function getValue(ret) {
254
+ if (typeof ret.fallbackValue !== "undefined") {
255
+ return typeof ret.fallbackValue === "object"
256
+ ? getValue(ret.fallbackValue)
257
+ : ret.fallbackValue;
258
+ }
259
+ else if (ret.el.tag === Portal) {
260
+ return;
261
+ }
262
+ else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) {
263
+ return ret.value;
264
+ }
265
+ return unwrap(getChildValues(ret));
266
+ }
267
+ /**
268
+ * Walks an element’s children to find its child values.
269
+ *
270
+ * @returns A normalized array of nodes and strings.
271
+ */
272
+ function getChildValues(ret) {
273
+ if (ret.cachedChildValues) {
274
+ return wrap(ret.cachedChildValues);
275
+ }
276
+ const values = [];
277
+ const children = wrap(ret.children);
278
+ for (let i = 0; i < children.length; i++) {
279
+ const child = children[i];
280
+ if (child) {
281
+ values.push(typeof child === "string" ? child : getValue(child));
282
+ }
283
+ }
284
+ const values1 = normalize(values);
285
+ const tag = ret.el.tag;
286
+ if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
287
+ ret.cachedChildValues = unwrap(values1);
288
+ }
289
+ return values1;
290
+ }
291
+ const defaultRendererImpl = {
292
+ create() {
293
+ throw new Error("Not implemented");
294
+ },
295
+ hydrate() {
296
+ throw new Error("Not implemented");
297
+ },
298
+ scope: IDENTITY,
299
+ read: IDENTITY,
300
+ text: IDENTITY,
301
+ raw: IDENTITY,
302
+ patch: NOOP,
303
+ arrange: NOOP,
304
+ dispose: NOOP,
305
+ flush: NOOP,
306
+ };
307
+ const _RendererImpl = Symbol.for("crank.RendererImpl");
308
+ /**
309
+ * An abstract class which is subclassed to render to different target
310
+ * environments. Subclasses will typically call super() with a custom
311
+ * RendererImpl. This class is responsible for kicking off the rendering
312
+ * process and caching previous trees by root.
313
+ *
314
+ * @template TNode - The type of the node for a rendering environment.
315
+ * @template TScope - Data which is passed down the tree.
316
+ * @template TRoot - The type of the root for a rendering environment.
317
+ * @template TResult - The type of exposed values.
318
+ */
319
+ class Renderer {
320
+ constructor(impl) {
321
+ this.cache = new WeakMap();
322
+ this[_RendererImpl] = {
323
+ ...defaultRendererImpl,
324
+ ...impl,
325
+ };
326
+ }
327
+ /**
328
+ * Renders an element tree into a specific root.
329
+ *
330
+ * @param children - An element tree. You can render null with a previously
331
+ * used root to delete the previously rendered element tree from the cache.
332
+ * @param root - The node to be rendered into. The renderer will cache
333
+ * element trees per root.
334
+ * @param bridge - An optional context that will be the ancestor context of all
335
+ * elements in the tree. Useful for connecting different renderers so that
336
+ * events/provisions properly propagate. The context for a given root must be
337
+ * the same or an error will be thrown.
338
+ *
339
+ * @returns The result of rendering the children, or a possible promise of
340
+ * the result if the element tree renders asynchronously.
341
+ */
342
+ render(children, root, bridge) {
343
+ let ret;
344
+ const ctx = bridge && bridge[_ContextImpl];
345
+ if (typeof root === "object" && root !== null) {
346
+ ret = this.cache.get(root);
347
+ }
348
+ let oldProps;
349
+ if (ret === undefined) {
350
+ ret = new Retainer(createElement(Portal, { children, root }));
351
+ ret.value = root;
352
+ ret.ctx = ctx;
353
+ if (typeof root === "object" && root !== null && children != null) {
354
+ this.cache.set(root, ret);
355
+ }
356
+ }
357
+ else if (ret.ctx !== ctx) {
358
+ throw new Error("Context mismatch");
359
+ }
360
+ else {
361
+ oldProps = ret.el.props;
362
+ ret.el = createElement(Portal, { children, root });
363
+ if (typeof root === "object" && root !== null && children == null) {
364
+ this.cache.delete(root);
365
+ }
366
+ }
367
+ const impl = this[_RendererImpl];
368
+ const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined);
369
+ // We return the child values of the portal because portal elements
370
+ // themselves have no readable value.
371
+ if (isPromiseLike(childValues)) {
372
+ return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
373
+ }
374
+ return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
375
+ }
376
+ hydrate(children, root, bridge) {
377
+ const impl = this[_RendererImpl];
378
+ const ctx = bridge && bridge[_ContextImpl];
379
+ let ret;
380
+ ret = this.cache.get(root);
381
+ if (ret !== undefined) {
382
+ // If there is a retainer for the root, hydration is not necessary.
383
+ return this.render(children, root, bridge);
384
+ }
385
+ let oldProps;
386
+ ret = new Retainer(createElement(Portal, { children, root }));
387
+ ret.value = root;
388
+ if (typeof root === "object" && root !== null && children != null) {
389
+ this.cache.set(root, ret);
390
+ }
391
+ const hydrationData = impl.hydrate(Portal, root, {});
392
+ const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData);
393
+ // We return the child values of the portal because portal elements
394
+ // themselves have no readable value.
395
+ if (isPromiseLike(childValues)) {
396
+ return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
397
+ }
398
+ return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
399
+ }
400
+ }
401
+ /*** PRIVATE RENDERER FUNCTIONS ***/
402
+ function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
403
+ // element is a host or portal element
404
+ if (root != null) {
405
+ renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
406
+ flush(renderer, root);
407
+ }
408
+ ret.cachedChildValues = unwrap(childValues);
409
+ if (root == null) {
410
+ unmount(renderer, ret, ctx, ret);
411
+ }
412
+ return renderer.read(ret.cachedChildValues);
413
+ }
414
+ function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) {
415
+ const oldRetained = wrap(parent.children);
416
+ const newRetained = [];
417
+ const newChildren = arrayify(children);
418
+ const values = [];
419
+ let graveyard;
420
+ let childrenByKey;
421
+ let seenKeys;
422
+ let isAsync = false;
423
+ // When hydrating, sibling element trees must be rendered in order, because
424
+ // we do not know how many DOM nodes an element will render.
425
+ let hydrationBlock;
426
+ let oi = 0;
427
+ let oldLength = oldRetained.length;
428
+ for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) {
429
+ // length checks to prevent index out of bounds deoptimizations.
430
+ let ret = oi >= oldLength ? undefined : oldRetained[oi];
431
+ let child = narrow(newChildren[ni]);
432
+ {
433
+ // aligning new children with old retainers
434
+ let oldKey = typeof ret === "object" ? ret.el.key : undefined;
435
+ let newKey = typeof child === "object" ? child.key : undefined;
436
+ if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) {
437
+ console.error("Duplicate key", newKey);
438
+ newKey = undefined;
439
+ }
440
+ if (oldKey === newKey) {
441
+ if (childrenByKey !== undefined && newKey !== undefined) {
442
+ childrenByKey.delete(newKey);
443
+ }
444
+ oi++;
445
+ }
446
+ else {
447
+ childrenByKey = childrenByKey || createChildrenByKey(oldRetained, oi);
448
+ if (newKey === undefined) {
449
+ while (ret !== undefined && oldKey !== undefined) {
450
+ oi++;
451
+ ret = oldRetained[oi];
452
+ oldKey = typeof ret === "object" ? ret.el.key : undefined;
453
+ }
454
+ oi++;
455
+ }
456
+ else {
457
+ ret = childrenByKey.get(newKey);
458
+ if (ret !== undefined) {
459
+ childrenByKey.delete(newKey);
460
+ }
461
+ (seenKeys = seenKeys || new Set()).add(newKey);
462
+ }
463
+ }
464
+ }
465
+ // Updating
466
+ let value;
467
+ if (typeof child === "object") {
468
+ if (child.tag === Copy || (typeof ret === "object" && ret.el === child)) {
469
+ value = getInflightValue(ret);
470
+ }
471
+ else {
472
+ let oldProps;
473
+ let copy = false;
474
+ if (typeof ret === "object" && ret.el.tag === child.tag) {
475
+ oldProps = ret.el.props;
476
+ ret.el = child;
477
+ if (child.copy) {
478
+ value = getInflightValue(ret);
479
+ copy = true;
480
+ }
481
+ }
482
+ else {
483
+ if (typeof ret === "object") {
484
+ (graveyard = graveyard || []).push(ret);
485
+ }
486
+ const fallback = ret;
487
+ ret = new Retainer(child);
488
+ ret.fallbackValue = fallback;
489
+ }
490
+ if (copy) ;
491
+ else if (child.tag === Raw) {
492
+ value = hydrationBlock
493
+ ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData))
494
+ : updateRaw(renderer, ret, scope, oldProps, hydrationData);
495
+ }
496
+ else if (child.tag === Fragment) {
497
+ value = hydrationBlock
498
+ ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData))
499
+ : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData);
500
+ }
501
+ else if (typeof child.tag === "function") {
502
+ value = hydrationBlock
503
+ ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData))
504
+ : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData);
505
+ }
506
+ else {
507
+ value = hydrationBlock
508
+ ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData))
509
+ : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData);
510
+ }
511
+ }
512
+ if (isPromiseLike(value)) {
513
+ isAsync = true;
514
+ if (hydrationData !== undefined) {
515
+ hydrationBlock = value;
516
+ }
517
+ }
518
+ }
519
+ else {
520
+ // child is a string or undefined
521
+ if (typeof ret === "object") {
522
+ (graveyard = graveyard || []).push(ret);
523
+ }
524
+ if (typeof child === "string") {
525
+ value = ret = renderer.text(child, scope, hydrationData);
526
+ }
527
+ else {
528
+ ret = undefined;
529
+ }
530
+ }
531
+ values[ni] = value;
532
+ newRetained[ni] = ret;
533
+ }
534
+ // cleanup remaining retainers
535
+ for (; oi < oldLength; oi++) {
536
+ const ret = oldRetained[oi];
537
+ if (typeof ret === "object" &&
538
+ (typeof ret.el.key === "undefined" ||
539
+ !seenKeys ||
540
+ !seenKeys.has(ret.el.key))) {
541
+ (graveyard = graveyard || []).push(ret);
542
+ }
543
+ }
544
+ if (childrenByKey !== undefined && childrenByKey.size > 0) {
545
+ (graveyard = graveyard || []).push(...childrenByKey.values());
546
+ }
547
+ parent.children = unwrap(newRetained);
548
+ if (isAsync) {
549
+ let childValues1 = Promise.all(values).finally(() => {
550
+ if (graveyard) {
551
+ for (let i = 0; i < graveyard.length; i++) {
552
+ unmount(renderer, host, ctx, graveyard[i]);
553
+ }
554
+ }
555
+ });
556
+ let onChildValues;
557
+ childValues1 = Promise.race([
558
+ childValues1,
559
+ new Promise((resolve) => (onChildValues = resolve)),
560
+ ]);
561
+ if (parent.onNextValues) {
562
+ parent.onNextValues(childValues1);
563
+ }
564
+ parent.onNextValues = onChildValues;
565
+ return childValues1.then((childValues) => {
566
+ parent.inflightValue = parent.fallbackValue = undefined;
567
+ return normalize(childValues);
568
+ });
569
+ }
570
+ else {
571
+ if (graveyard) {
572
+ for (let i = 0; i < graveyard.length; i++) {
573
+ unmount(renderer, host, ctx, graveyard[i]);
574
+ }
575
+ }
576
+ if (parent.onNextValues) {
577
+ parent.onNextValues(values);
578
+ parent.onNextValues = undefined;
579
+ }
580
+ parent.inflightValue = parent.fallbackValue = undefined;
581
+ // We can assert there are no promises in the array because isAsync is false
582
+ return normalize(values);
583
+ }
584
+ }
585
+ function createChildrenByKey(children, offset) {
586
+ const childrenByKey = new Map();
587
+ for (let i = offset; i < children.length; i++) {
588
+ const child = children[i];
589
+ if (typeof child === "object" && typeof child.el.key !== "undefined") {
590
+ childrenByKey.set(child.el.key, child);
591
+ }
592
+ }
593
+ return childrenByKey;
594
+ }
595
+ function getInflightValue(child) {
596
+ if (typeof child !== "object") {
597
+ return child;
598
+ }
599
+ const ctx = typeof child.el.tag === "function" ? child.ctx : undefined;
600
+ if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
601
+ return ctx.inflightValue;
602
+ }
603
+ else if (child.inflightValue) {
604
+ return child.inflightValue;
605
+ }
606
+ return getValue(child);
607
+ }
608
+ function updateRaw(renderer, ret, scope, oldProps, hydrationData) {
609
+ const props = ret.el.props;
610
+ if (!oldProps || oldProps.value !== props.value) {
611
+ ret.value = renderer.raw(props.value, scope, hydrationData);
612
+ if (typeof ret.el.ref === "function") {
613
+ ret.el.ref(ret.value);
614
+ }
615
+ }
616
+ return ret.value;
617
+ }
618
+ function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) {
619
+ const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData);
620
+ if (isPromiseLike(childValues)) {
621
+ ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
622
+ return ret.inflightValue;
623
+ }
624
+ return unwrap(childValues);
625
+ }
626
+ function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) {
627
+ const el = ret.el;
628
+ const tag = el.tag;
629
+ let hydrationValue;
630
+ if (el.tag === Portal) {
631
+ root = ret.value = el.props.root;
632
+ }
633
+ else {
634
+ if (hydrationData !== undefined) {
635
+ const value = hydrationData.children.shift();
636
+ hydrationValue = value;
637
+ }
638
+ }
639
+ scope = renderer.scope(scope, tag, el.props);
640
+ let childHydrationData;
641
+ if (hydrationValue != null && typeof hydrationValue !== "string") {
642
+ childHydrationData = renderer.hydrate(tag, hydrationValue, el.props);
643
+ if (childHydrationData === undefined) {
644
+ hydrationValue = undefined;
645
+ }
646
+ }
647
+ const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData);
648
+ if (isPromiseLike(childValues)) {
649
+ ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue));
650
+ return ret.inflightValue;
651
+ }
652
+ return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue);
653
+ }
654
+ function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) {
655
+ const tag = ret.el.tag;
656
+ let value = ret.value;
657
+ if (hydrationValue != null) {
658
+ value = ret.value = hydrationValue;
659
+ if (typeof ret.el.ref === "function") {
660
+ ret.el.ref(value);
661
+ }
662
+ }
663
+ let props = ret.el.props;
664
+ let copied;
665
+ if (tag !== Portal) {
666
+ if (value == null) {
667
+ // This assumes that renderer.create does not return nullish values.
668
+ value = ret.value = renderer.create(tag, props, scope);
669
+ if (typeof ret.el.ref === "function") {
670
+ ret.el.ref(value);
671
+ }
672
+ }
673
+ for (const propName in { ...oldProps, ...props }) {
674
+ const propValue = props[propName];
675
+ if (propValue === Copy) {
676
+ // TODO: The Copy tag doubles as a way to skip the patching of a prop.
677
+ // Not sure about this feature. Should probably be removed.
678
+ (copied = copied || new Set()).add(propName);
679
+ }
680
+ else if (!SPECIAL_PROPS.has(propName)) {
681
+ renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope);
682
+ }
683
+ }
684
+ }
685
+ if (copied) {
686
+ props = { ...ret.el.props };
687
+ for (const name of copied) {
688
+ props[name] = oldProps && oldProps[name];
689
+ }
690
+ ret.el = new Element(tag, props);
691
+ }
692
+ renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
693
+ ret.cachedChildValues = unwrap(childValues);
694
+ if (tag === Portal) {
695
+ flush(renderer, ret.value);
696
+ return;
697
+ }
698
+ return value;
699
+ }
700
+ function flush(renderer, root, initiator) {
701
+ renderer.flush(root);
702
+ if (typeof root !== "object" || root === null) {
703
+ return;
704
+ }
705
+ const flushMap = flushMaps.get(root);
706
+ if (flushMap) {
707
+ if (initiator) {
708
+ const flushMap1 = new Map();
709
+ for (let [ctx, callbacks] of flushMap) {
710
+ if (!ctxContains(initiator, ctx)) {
711
+ flushMap.delete(ctx);
712
+ flushMap1.set(ctx, callbacks);
713
+ }
714
+ }
715
+ if (flushMap1.size) {
716
+ flushMaps.set(root, flushMap1);
717
+ }
718
+ else {
719
+ flushMaps.delete(root);
720
+ }
721
+ }
722
+ else {
723
+ flushMaps.delete(root);
724
+ }
725
+ for (const [ctx, callbacks] of flushMap) {
726
+ const value = renderer.read(getValue(ctx.ret));
727
+ for (const callback of callbacks) {
728
+ callback(value);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ function unmount(renderer, host, ctx, ret) {
734
+ if (typeof ret.el.tag === "function") {
735
+ ctx = ret.ctx;
736
+ unmountComponent(ctx);
737
+ }
738
+ else if (ret.el.tag === Portal) {
739
+ host = ret;
740
+ renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
741
+ flush(renderer, host.value);
742
+ }
743
+ else if (ret.el.tag !== Fragment) {
744
+ if (isEventTarget(ret.value)) {
745
+ const records = getListenerRecords(ctx, host);
746
+ for (let i = 0; i < records.length; i++) {
747
+ const record = records[i];
748
+ ret.value.removeEventListener(record.type, record.callback, record.options);
749
+ }
750
+ }
751
+ renderer.dispose(ret.el.tag, ret.value, ret.el.props);
752
+ host = ret;
753
+ }
754
+ const children = wrap(ret.children);
755
+ for (let i = 0; i < children.length; i++) {
756
+ const child = children[i];
757
+ if (typeof child === "object") {
758
+ unmount(renderer, host, ctx, child);
759
+ }
760
+ }
761
+ }
762
+ /*** CONTEXT FLAGS ***/
763
+ /**
764
+ * A flag which is true when the component is initialized or updated by an
765
+ * ancestor component or the root render call.
766
+ *
767
+ * Used to determine things like whether the nearest host ancestor needs to be
768
+ * rearranged.
769
+ */
770
+ const IsUpdating = 1 << 0;
771
+ /**
772
+ * A flag which is true when the component is synchronously executing.
773
+ *
774
+ * Used to guard against components triggering stack overflow or generator error.
775
+ */
776
+ const IsSyncExecuting = 1 << 1;
777
+ /**
778
+ * A flag which is true when the component is in a for...of loop.
779
+ */
780
+ const IsInForOfLoop = 1 << 2;
781
+ /**
782
+ * A flag which is true when the component is in a for await...of loop.
783
+ */
784
+ const IsInForAwaitOfLoop = 1 << 3;
785
+ /**
786
+ * A flag which is true when the component starts the render loop but has not
787
+ * yielded yet.
788
+ *
789
+ * Used to make sure that components yield at least once per loop.
790
+ */
791
+ const NeedsToYield = 1 << 4;
792
+ /**
793
+ * A flag used by async generator components in conjunction with the
794
+ * onAvailable callback to mark whether new props can be pulled via the context
795
+ * async iterator. See the Symbol.asyncIterator method and the
796
+ * resumeCtxIterator function.
797
+ */
798
+ const PropsAvailable = 1 << 5;
799
+ /**
800
+ * A flag which is set when a component errors.
801
+ *
802
+ * This is mainly used to prevent some false positives in "component yields or
803
+ * returns undefined" warnings. The reason we’re using this versus IsUnmounted
804
+ * is a very troubling test (cascades sync generator parent and sync generator
805
+ * child) where synchronous code causes a stack overflow error in a
806
+ * non-deterministic way. Deeply disturbing stuff.
807
+ */
808
+ const IsErrored = 1 << 6;
809
+ /**
810
+ * A flag which is set when the component is unmounted. Unmounted components
811
+ * are no longer in the element tree and cannot refresh or rerender.
812
+ */
813
+ const IsUnmounted = 1 << 7;
814
+ /**
815
+ * A flag which indicates that the component is a sync generator component.
816
+ */
817
+ const IsSyncGen = 1 << 8;
818
+ /**
819
+ * A flag which indicates that the component is an async generator component.
820
+ */
821
+ const IsAsyncGen = 1 << 9;
822
+ /**
823
+ * A flag which is set while schedule callbacks are called.
824
+ */
825
+ const IsScheduling = 1 << 10;
826
+ /**
827
+ * A flag which is set when a schedule callback calls refresh.
828
+ */
829
+ const IsSchedulingRefresh = 1 << 11;
830
+ const provisionMaps = new WeakMap();
831
+ const scheduleMap = new WeakMap();
832
+ const cleanupMap = new WeakMap();
833
+ // keys are roots
834
+ const flushMaps = new WeakMap();
835
+ /**
836
+ * @internal
837
+ * The internal class which holds context data.
838
+ */
839
+ class ContextImpl {
840
+ constructor(renderer, root, host, parent, scope, ret) {
841
+ this.f = 0;
842
+ this.owner = new Context(this);
843
+ this.renderer = renderer;
844
+ this.root = root;
845
+ this.host = host;
846
+ this.parent = parent;
847
+ this.scope = scope;
848
+ this.ret = ret;
849
+ this.iterator = undefined;
850
+ this.inflightBlock = undefined;
851
+ this.inflightValue = undefined;
852
+ this.enqueuedBlock = undefined;
853
+ this.enqueuedValue = undefined;
854
+ this.onProps = undefined;
855
+ this.onPropsRequested = undefined;
856
+ }
857
+ }
858
+ const _ContextImpl = Symbol.for("crank.ContextImpl");
859
+ /**
860
+ * A class which is instantiated and passed to every component as its this
861
+ * value. Contexts form a tree just like elements and all components in the
862
+ * element tree are connected via contexts. Components can use this tree to
863
+ * communicate data upwards via events and downwards via provisions.
864
+ *
865
+ * @template [T=*] - The expected shape of the props passed to the component,
866
+ * or a component function. Used to strongly type the Context iterator methods.
867
+ * @template [TResult=*] - The readable element value type. It is used in
868
+ * places such as the return value of refresh and the argument passed to
869
+ * schedule and cleanup callbacks.
870
+ */
871
+ class Context {
872
+ // TODO: If we could make the constructor function take a nicer value, it
873
+ // would be useful for testing purposes.
874
+ constructor(impl) {
875
+ this[_ContextImpl] = impl;
876
+ }
877
+ /**
878
+ * The current props of the associated element.
879
+ */
880
+ get props() {
881
+ return this[_ContextImpl].ret.el.props;
882
+ }
883
+ /**
884
+ * The current value of the associated element.
885
+ *
886
+ * @deprecated
887
+ */
888
+ get value() {
889
+ return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
890
+ }
891
+ *[Symbol.iterator]() {
892
+ const ctx = this[_ContextImpl];
893
+ try {
894
+ ctx.f |= IsInForOfLoop;
895
+ while (!(ctx.f & IsUnmounted)) {
896
+ if (ctx.f & NeedsToYield) {
897
+ throw new Error("Context iterated twice without a yield");
898
+ }
899
+ else {
900
+ ctx.f |= NeedsToYield;
901
+ }
902
+ yield ctx.ret.el.props;
903
+ }
904
+ }
905
+ finally {
906
+ ctx.f &= -5;
907
+ }
908
+ }
909
+ async *[Symbol.asyncIterator]() {
910
+ const ctx = this[_ContextImpl];
911
+ if (ctx.f & IsSyncGen) {
912
+ throw new Error("Use for...of in sync generator components");
913
+ }
914
+ try {
915
+ ctx.f |= IsInForAwaitOfLoop;
916
+ while (!(ctx.f & IsUnmounted)) {
917
+ if (ctx.f & NeedsToYield) {
918
+ throw new Error("Context iterated twice without a yield");
919
+ }
920
+ else {
921
+ ctx.f |= NeedsToYield;
922
+ }
923
+ if (ctx.f & PropsAvailable) {
924
+ ctx.f &= ~PropsAvailable;
925
+ yield ctx.ret.el.props;
926
+ }
927
+ else {
928
+ const props = await new Promise((resolve) => (ctx.onProps = resolve));
929
+ if (ctx.f & IsUnmounted) {
930
+ break;
931
+ }
932
+ yield props;
933
+ }
934
+ if (ctx.onPropsRequested) {
935
+ ctx.onPropsRequested();
936
+ ctx.onPropsRequested = undefined;
937
+ }
938
+ }
939
+ }
940
+ finally {
941
+ ctx.f &= -9;
942
+ if (ctx.onPropsRequested) {
943
+ ctx.onPropsRequested();
944
+ ctx.onPropsRequested = undefined;
945
+ }
946
+ }
947
+ }
948
+ /**
949
+ * Re-executes a component.
950
+ *
951
+ * @returns The rendered value of the component or a promise thereof if the
952
+ * component or its children execute asynchronously.
953
+ *
954
+ * The refresh method works a little differently for async generator
955
+ * components, in that it will resume the Context’s props async iterator
956
+ * rather than resuming execution. This is because async generator components
957
+ * are perpetually resumed independent of updates, and rely on the props
958
+ * async iterator to suspend.
959
+ */
960
+ refresh() {
961
+ const ctx = this[_ContextImpl];
962
+ if (ctx.f & IsUnmounted) {
963
+ console.error("Component is unmounted");
964
+ return ctx.renderer.read(undefined);
965
+ }
966
+ else if (ctx.f & IsSyncExecuting) {
967
+ console.error("Component is already executing");
968
+ return ctx.renderer.read(getValue(ctx.ret));
969
+ }
970
+ const value = enqueueComponentRun(ctx);
971
+ if (isPromiseLike(value)) {
972
+ return value.then((value) => ctx.renderer.read(value));
973
+ }
974
+ return ctx.renderer.read(value);
975
+ }
976
+ /**
977
+ * Registers a callback which fires when the component commits. Will only
978
+ * fire once per callback and update.
979
+ */
980
+ schedule(callback) {
981
+ const ctx = this[_ContextImpl];
982
+ let callbacks = scheduleMap.get(ctx);
983
+ if (!callbacks) {
984
+ callbacks = new Set();
985
+ scheduleMap.set(ctx, callbacks);
986
+ }
987
+ callbacks.add(callback);
988
+ }
989
+ /**
990
+ * Registers a callback which fires when the component’s children are
991
+ * rendered into the root. Will only fire once per callback and render.
992
+ */
993
+ flush(callback) {
994
+ const ctx = this[_ContextImpl];
995
+ if (typeof ctx.root !== "object" || ctx.root === null) {
996
+ return;
997
+ }
998
+ let flushMap = flushMaps.get(ctx.root);
999
+ if (!flushMap) {
1000
+ flushMap = new Map();
1001
+ flushMaps.set(ctx.root, flushMap);
1002
+ }
1003
+ let callbacks = flushMap.get(ctx);
1004
+ if (!callbacks) {
1005
+ callbacks = new Set();
1006
+ flushMap.set(ctx, callbacks);
1007
+ }
1008
+ callbacks.add(callback);
1009
+ }
1010
+ /**
1011
+ * Registers a callback which fires when the component unmounts. Will only
1012
+ * fire once per callback.
1013
+ */
1014
+ cleanup(callback) {
1015
+ const ctx = this[_ContextImpl];
1016
+ if (ctx.f & IsUnmounted) {
1017
+ const value = ctx.renderer.read(getValue(ctx.ret));
1018
+ callback(value);
1019
+ return;
1020
+ }
1021
+ let callbacks = cleanupMap.get(ctx);
1022
+ if (!callbacks) {
1023
+ callbacks = new Set();
1024
+ cleanupMap.set(ctx, callbacks);
1025
+ }
1026
+ callbacks.add(callback);
1027
+ }
1028
+ consume(key) {
1029
+ for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
1030
+ const provisions = provisionMaps.get(ctx);
1031
+ if (provisions && provisions.has(key)) {
1032
+ return provisions.get(key);
1033
+ }
1034
+ }
1035
+ }
1036
+ provide(key, value) {
1037
+ const ctx = this[_ContextImpl];
1038
+ let provisions = provisionMaps.get(ctx);
1039
+ if (!provisions) {
1040
+ provisions = new Map();
1041
+ provisionMaps.set(ctx, provisions);
1042
+ }
1043
+ provisions.set(key, value);
1044
+ }
1045
+ addEventListener(type, listener, options) {
1046
+ const ctx = this[_ContextImpl];
1047
+ let listeners;
1048
+ if (!isListenerOrListenerObject(listener)) {
1049
+ return;
1050
+ }
1051
+ else {
1052
+ const listeners1 = listenersMap.get(ctx);
1053
+ if (listeners1) {
1054
+ listeners = listeners1;
1055
+ }
1056
+ else {
1057
+ listeners = [];
1058
+ listenersMap.set(ctx, listeners);
1059
+ }
1060
+ }
1061
+ options = normalizeListenerOptions(options);
1062
+ let callback;
1063
+ if (typeof listener === "object") {
1064
+ callback = () => listener.handleEvent.apply(listener, arguments);
1065
+ }
1066
+ else {
1067
+ callback = listener;
1068
+ }
1069
+ const record = { type, listener, callback, options };
1070
+ if (options.once) {
1071
+ record.callback = function () {
1072
+ const i = listeners.indexOf(record);
1073
+ if (i !== -1) {
1074
+ listeners.splice(i, 1);
1075
+ }
1076
+ return callback.apply(this, arguments);
1077
+ };
1078
+ }
1079
+ if (listeners.some((record1) => record.type === record1.type &&
1080
+ record.listener === record1.listener &&
1081
+ !record.options.capture === !record1.options.capture)) {
1082
+ return;
1083
+ }
1084
+ listeners.push(record);
1085
+ // TODO: is it possible to separate out the EventTarget delegation logic
1086
+ for (const value of getChildValues(ctx.ret)) {
1087
+ if (isEventTarget(value)) {
1088
+ value.addEventListener(record.type, record.callback, record.options);
1089
+ }
1090
+ }
1091
+ }
1092
+ removeEventListener(type, listener, options) {
1093
+ const ctx = this[_ContextImpl];
1094
+ const listeners = listenersMap.get(ctx);
1095
+ if (listeners == null || !isListenerOrListenerObject(listener)) {
1096
+ return;
1097
+ }
1098
+ const options1 = normalizeListenerOptions(options);
1099
+ const i = listeners.findIndex((record) => record.type === type &&
1100
+ record.listener === listener &&
1101
+ !record.options.capture === !options1.capture);
1102
+ if (i === -1) {
1103
+ return;
1104
+ }
1105
+ const record = listeners[i];
1106
+ listeners.splice(i, 1);
1107
+ // TODO: is it possible to separate out the EventTarget delegation logic
1108
+ for (const value of getChildValues(ctx.ret)) {
1109
+ if (isEventTarget(value)) {
1110
+ value.removeEventListener(record.type, record.callback, record.options);
1111
+ }
1112
+ }
1113
+ }
1114
+ dispatchEvent(ev) {
1115
+ const ctx = this[_ContextImpl];
1116
+ const path = [];
1117
+ for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1118
+ path.push(parent);
1119
+ }
1120
+ // We patch the stopImmediatePropagation method because ev.cancelBubble
1121
+ // only informs us if stopPropagation was called and there are no
1122
+ // properties which inform us if stopImmediatePropagation was called.
1123
+ let immediateCancelBubble = false;
1124
+ const stopImmediatePropagation = ev.stopImmediatePropagation;
1125
+ setEventProperty(ev, "stopImmediatePropagation", () => {
1126
+ immediateCancelBubble = true;
1127
+ return stopImmediatePropagation.call(ev);
1128
+ });
1129
+ setEventProperty(ev, "target", ctx.owner);
1130
+ // The only possible errors in this block are errors thrown by callbacks,
1131
+ // and dispatchEvent will only log these errors rather than throwing
1132
+ // them. Therefore, we place all code in a try block, log errors in the
1133
+ // catch block, and use an unsafe return statement in the finally block.
1134
+ //
1135
+ // Each early return within the try block returns true because while the
1136
+ // return value is overridden in the finally block, TypeScript
1137
+ // (justifiably) does not recognize the unsafe return statement.
1138
+ try {
1139
+ setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1140
+ for (let i = path.length - 1; i >= 0; i--) {
1141
+ const target = path[i];
1142
+ const listeners = listenersMap.get(target);
1143
+ if (listeners) {
1144
+ setEventProperty(ev, "currentTarget", target.owner);
1145
+ for (const record of listeners) {
1146
+ if (record.type === ev.type && record.options.capture) {
1147
+ try {
1148
+ record.callback.call(target.owner, ev);
1149
+ }
1150
+ catch (err) {
1151
+ console.error(err);
1152
+ }
1153
+ if (immediateCancelBubble) {
1154
+ return true;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ if (ev.cancelBubble) {
1160
+ return true;
1161
+ }
1162
+ }
1163
+ {
1164
+ setEventProperty(ev, "eventPhase", AT_TARGET);
1165
+ setEventProperty(ev, "currentTarget", ctx.owner);
1166
+ // dispatchEvent calls the prop callback if it exists
1167
+ let propCallback = ctx.ret.el.props["on" + ev.type];
1168
+ if (typeof propCallback === "function") {
1169
+ propCallback(ev);
1170
+ if (immediateCancelBubble || ev.cancelBubble) {
1171
+ return true;
1172
+ }
1173
+ }
1174
+ else {
1175
+ // Checks for camel-cased event props
1176
+ for (const propName in ctx.ret.el.props) {
1177
+ if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) {
1178
+ propCallback = ctx.ret.el.props[propName];
1179
+ if (typeof propCallback === "function") {
1180
+ propCallback(ev);
1181
+ if (immediateCancelBubble || ev.cancelBubble) {
1182
+ return true;
1183
+ }
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ const listeners = listenersMap.get(ctx);
1189
+ if (listeners) {
1190
+ for (const record of listeners) {
1191
+ if (record.type === ev.type) {
1192
+ try {
1193
+ record.callback.call(ctx.owner, ev);
1194
+ }
1195
+ catch (err) {
1196
+ console.error(err);
1197
+ }
1198
+ if (immediateCancelBubble) {
1199
+ return true;
1200
+ }
1201
+ }
1202
+ }
1203
+ if (ev.cancelBubble) {
1204
+ return true;
1205
+ }
1206
+ }
1207
+ }
1208
+ if (ev.bubbles) {
1209
+ setEventProperty(ev, "eventPhase", BUBBLING_PHASE);
1210
+ for (let i = 0; i < path.length; i++) {
1211
+ const target = path[i];
1212
+ const listeners = listenersMap.get(target);
1213
+ if (listeners) {
1214
+ setEventProperty(ev, "currentTarget", target.owner);
1215
+ for (const record of listeners) {
1216
+ if (record.type === ev.type && !record.options.capture) {
1217
+ try {
1218
+ record.callback.call(target.owner, ev);
1219
+ }
1220
+ catch (err) {
1221
+ console.error(err);
1222
+ }
1223
+ if (immediateCancelBubble) {
1224
+ return true;
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ if (ev.cancelBubble) {
1230
+ return true;
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ finally {
1236
+ setEventProperty(ev, "eventPhase", NONE);
1237
+ setEventProperty(ev, "currentTarget", null);
1238
+ // eslint-disable-next-line no-unsafe-finally
1239
+ return !ev.defaultPrevented;
1240
+ }
1241
+ }
1242
+ }
1243
+ /*** PRIVATE CONTEXT FUNCTIONS ***/
1244
+ function ctxContains(parent, child) {
1245
+ for (let current = child; current !== undefined; current = current.parent) {
1246
+ if (current === parent) {
1247
+ return true;
1248
+ }
1249
+ }
1250
+ return false;
1251
+ }
1252
+ function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) {
1253
+ let ctx;
1254
+ if (oldProps) {
1255
+ ctx = ret.ctx;
1256
+ if (ctx.f & IsSyncExecuting) {
1257
+ console.error("Component is already executing");
1258
+ return ret.cachedChildValues;
1259
+ }
1260
+ }
1261
+ else {
1262
+ ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1263
+ }
1264
+ ctx.f |= IsUpdating;
1265
+ return enqueueComponentRun(ctx, hydrationData);
1266
+ }
1267
+ function updateComponentChildren(ctx, children, hydrationData) {
1268
+ if (ctx.f & IsUnmounted) {
1269
+ return;
1270
+ }
1271
+ else if (ctx.f & IsErrored) {
1272
+ // This branch is necessary for some race conditions where this function is
1273
+ // called after iterator.throw() in async generator components.
1274
+ return;
1275
+ }
1276
+ else if (children === undefined) {
1277
+ console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1278
+ }
1279
+ let childValues;
1280
+ try {
1281
+ // TODO: WAT
1282
+ // We set the isExecuting flag in case a child component dispatches an event
1283
+ // which bubbles to this component and causes a synchronous refresh().
1284
+ ctx.f |= IsSyncExecuting;
1285
+ childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData);
1286
+ }
1287
+ finally {
1288
+ ctx.f &= -3;
1289
+ }
1290
+ if (isPromiseLike(childValues)) {
1291
+ ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1292
+ return ctx.ret.inflightValue;
1293
+ }
1294
+ return commitComponent(ctx, childValues);
1295
+ }
1296
+ function commitComponent(ctx, values) {
1297
+ if (ctx.f & IsUnmounted) {
1298
+ return;
1299
+ }
1300
+ const listeners = listenersMap.get(ctx);
1301
+ if (listeners && listeners.length) {
1302
+ for (let i = 0; i < values.length; i++) {
1303
+ const value = values[i];
1304
+ if (isEventTarget(value)) {
1305
+ for (let j = 0; j < listeners.length; j++) {
1306
+ const record = listeners[j];
1307
+ value.addEventListener(record.type, record.callback, record.options);
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ const oldValues = wrap(ctx.ret.cachedChildValues);
1313
+ let value = (ctx.ret.cachedChildValues = unwrap(values));
1314
+ if (ctx.f & IsScheduling) {
1315
+ ctx.f |= IsSchedulingRefresh;
1316
+ }
1317
+ else if (!(ctx.f & IsUpdating)) {
1318
+ // If we’re not updating the component, which happens when components are
1319
+ // refreshed, or when async generator components iterate, we have to do a
1320
+ // little bit housekeeping when a component’s child values have changed.
1321
+ if (!arrayEqual(oldValues, values)) {
1322
+ const records = getListenerRecords(ctx.parent, ctx.host);
1323
+ if (records.length) {
1324
+ for (let i = 0; i < values.length; i++) {
1325
+ const value = values[i];
1326
+ if (isEventTarget(value)) {
1327
+ for (let j = 0; j < records.length; j++) {
1328
+ const record = records[j];
1329
+ value.addEventListener(record.type, record.callback, record.options);
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+ // rearranging the nearest ancestor host element
1335
+ const host = ctx.host;
1336
+ const oldHostValues = wrap(host.cachedChildValues);
1337
+ invalidate(ctx, host);
1338
+ const hostValues = getChildValues(host);
1339
+ ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
1340
+ // props and oldProps are the same because the host isn’t updated.
1341
+ host.el.props, oldHostValues);
1342
+ }
1343
+ flush(ctx.renderer, ctx.root, ctx);
1344
+ }
1345
+ const callbacks = scheduleMap.get(ctx);
1346
+ if (callbacks) {
1347
+ scheduleMap.delete(ctx);
1348
+ ctx.f |= IsScheduling;
1349
+ const value1 = ctx.renderer.read(value);
1350
+ for (const callback of callbacks) {
1351
+ callback(value1);
1352
+ }
1353
+ ctx.f &= -1025;
1354
+ // Handles an edge case where refresh() is called during a schedule().
1355
+ if (ctx.f & IsSchedulingRefresh) {
1356
+ ctx.f &= -2049;
1357
+ value = getValue(ctx.ret);
1358
+ }
1359
+ }
1360
+ ctx.f &= -2;
1361
+ return value;
1362
+ }
1363
+ function invalidate(ctx, host) {
1364
+ for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1365
+ parent.ret.cachedChildValues = undefined;
1366
+ }
1367
+ host.cachedChildValues = undefined;
1368
+ }
1369
+ function arrayEqual(arr1, arr2) {
1370
+ if (arr1.length !== arr2.length) {
1371
+ return false;
1372
+ }
1373
+ for (let i = 0; i < arr1.length; i++) {
1374
+ const value1 = arr1[i];
1375
+ const value2 = arr2[i];
1376
+ if (value1 !== value2) {
1377
+ return false;
1378
+ }
1379
+ }
1380
+ return true;
1381
+ }
1382
+ /** Enqueues and executes the component associated with the context. */
1383
+ function enqueueComponentRun(ctx, hydrationData) {
1384
+ if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1385
+ if (hydrationData !== undefined) {
1386
+ throw new Error("Hydration error");
1387
+ }
1388
+ // This branch will run for non-initial renders of async generator
1389
+ // components when they are not in for...of loops. When in a for...of loop,
1390
+ // async generator components will behave normally.
1391
+ //
1392
+ // Async gen componennts can be in one of three states:
1393
+ //
1394
+ // 1. propsAvailable flag is true: "available"
1395
+ //
1396
+ // The component is suspended somewhere in the loop. When the component
1397
+ // reaches the bottom of the loop, it will run again with the next props.
1398
+ //
1399
+ // 2. onAvailable callback is defined: "suspended"
1400
+ //
1401
+ // The component has suspended at the bottom of the loop and is waiting
1402
+ // for new props.
1403
+ //
1404
+ // 3. neither 1 or 2: "Running"
1405
+ //
1406
+ // The component is suspended somewhere in the loop. When the component
1407
+ // reaches the bottom of the loop, it will suspend.
1408
+ //
1409
+ // Components will never be both available and suspended at
1410
+ // the same time.
1411
+ //
1412
+ // If the component is at the loop bottom, this means that the next value
1413
+ // produced by the component will have the most up to date props, so we can
1414
+ // simply return the current inflight value. Otherwise, we have to wait for
1415
+ // the bottom of the loop to be reached before returning the inflight
1416
+ // value.
1417
+ const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
1418
+ resumePropsAsyncIterator(ctx);
1419
+ if (isAtLoopbottom) {
1420
+ if (ctx.inflightBlock == null) {
1421
+ ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1422
+ }
1423
+ return ctx.inflightBlock.then(() => {
1424
+ ctx.inflightBlock = undefined;
1425
+ return ctx.inflightValue;
1426
+ });
1427
+ }
1428
+ return ctx.inflightValue;
1429
+ }
1430
+ else if (!ctx.inflightBlock) {
1431
+ try {
1432
+ const [block, value] = runComponent(ctx, hydrationData);
1433
+ if (block) {
1434
+ ctx.inflightBlock = block
1435
+ // TODO: there is some fuckery going on here related to async
1436
+ // generator components resuming when they’re meant to be returned.
1437
+ .then((v) => v)
1438
+ .finally(() => advanceComponent(ctx));
1439
+ // stepComponent will only return a block if the value is asynchronous
1440
+ ctx.inflightValue = value;
1441
+ }
1442
+ return value;
1443
+ }
1444
+ catch (err) {
1445
+ if (!(ctx.f & IsUpdating)) {
1446
+ if (!ctx.parent) {
1447
+ throw err;
1448
+ }
1449
+ return propagateError(ctx.parent, err);
1450
+ }
1451
+ throw err;
1452
+ }
1453
+ }
1454
+ else if (!ctx.enqueuedBlock) {
1455
+ if (hydrationData !== undefined) {
1456
+ throw new Error("Hydration error");
1457
+ }
1458
+ // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1459
+ // the Promise constructor call here.
1460
+ let resolveEnqueuedBlock;
1461
+ ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1462
+ ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1463
+ try {
1464
+ const [block, value] = runComponent(ctx);
1465
+ if (block) {
1466
+ resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1467
+ }
1468
+ return value;
1469
+ }
1470
+ catch (err) {
1471
+ if (!(ctx.f & IsUpdating)) {
1472
+ if (!ctx.parent) {
1473
+ throw err;
1474
+ }
1475
+ return propagateError(ctx.parent, err);
1476
+ }
1477
+ throw err;
1478
+ }
1479
+ });
1480
+ }
1481
+ return ctx.enqueuedValue;
1482
+ }
1483
+ /** Called when the inflight block promise settles. */
1484
+ function advanceComponent(ctx) {
1485
+ if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1486
+ return;
1487
+ }
1488
+ ctx.inflightBlock = ctx.enqueuedBlock;
1489
+ ctx.inflightValue = ctx.enqueuedValue;
1490
+ ctx.enqueuedBlock = undefined;
1491
+ ctx.enqueuedValue = undefined;
1492
+ }
1493
+ /**
1494
+ * This function is responsible for executing the component and handling all
1495
+ * the different component types. We cannot identify whether a component is a
1496
+ * generator or async without calling it and inspecting the return value.
1497
+ *
1498
+ * @returns {[block, value]} A tuple where
1499
+ * block - A possible promise which represents the duration during which the
1500
+ * component is blocked from updating.
1501
+ * value - A possible promise resolving to the rendered value of children.
1502
+ *
1503
+ * Each component type will block according to the type of the component.
1504
+ * - Sync function components never block and will transparently pass updates
1505
+ * to children.
1506
+ * - Async function components and async generator components block while
1507
+ * executing itself, but will not block for async children.
1508
+ * - Sync generator components block while any children are executing, because
1509
+ * they are expected to only resume when they’ve actually rendered.
1510
+ */
1511
+ function runComponent(ctx, hydrationData) {
1512
+ const ret = ctx.ret;
1513
+ const initial = !ctx.iterator;
1514
+ if (initial) {
1515
+ resumePropsAsyncIterator(ctx);
1516
+ ctx.f |= IsSyncExecuting;
1517
+ clearEventListeners(ctx);
1518
+ let result;
1519
+ try {
1520
+ result = ret.el.tag.call(ctx.owner, ret.el.props, ctx.owner);
1521
+ }
1522
+ catch (err) {
1523
+ ctx.f |= IsErrored;
1524
+ throw err;
1525
+ }
1526
+ finally {
1527
+ ctx.f &= -3;
1528
+ }
1529
+ if (isIteratorLike(result)) {
1530
+ ctx.iterator = result;
1531
+ }
1532
+ else if (isPromiseLike(result)) {
1533
+ // async function component
1534
+ const result1 = result instanceof Promise ? result : Promise.resolve(result);
1535
+ const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => {
1536
+ ctx.f |= IsErrored;
1537
+ throw err;
1538
+ });
1539
+ return [result1.catch(NOOP), value];
1540
+ }
1541
+ else {
1542
+ // sync function component
1543
+ return [
1544
+ undefined,
1545
+ updateComponentChildren(ctx, result, hydrationData),
1546
+ ];
1547
+ }
1548
+ }
1549
+ else if (hydrationData !== undefined) {
1550
+ // hydration data should only be passed on the initial render
1551
+ throw new Error("Hydration error");
1552
+ }
1553
+ let iteration;
1554
+ if (initial) {
1555
+ try {
1556
+ ctx.f |= IsSyncExecuting;
1557
+ iteration = ctx.iterator.next();
1558
+ }
1559
+ catch (err) {
1560
+ ctx.f |= IsErrored;
1561
+ throw err;
1562
+ }
1563
+ finally {
1564
+ ctx.f &= -3;
1565
+ }
1566
+ if (isPromiseLike(iteration)) {
1567
+ ctx.f |= IsAsyncGen;
1568
+ }
1569
+ else {
1570
+ ctx.f |= IsSyncGen;
1571
+ }
1572
+ }
1573
+ if (ctx.f & IsSyncGen) {
1574
+ // sync generator component
1575
+ if (!initial) {
1576
+ try {
1577
+ ctx.f |= IsSyncExecuting;
1578
+ iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1579
+ }
1580
+ catch (err) {
1581
+ ctx.f |= IsErrored;
1582
+ throw err;
1583
+ }
1584
+ finally {
1585
+ ctx.f &= -3;
1586
+ }
1587
+ }
1588
+ if (isPromiseLike(iteration)) {
1589
+ throw new Error("Mixed generator component");
1590
+ }
1591
+ if (ctx.f & IsInForOfLoop &&
1592
+ !(ctx.f & NeedsToYield) &&
1593
+ !(ctx.f & IsUnmounted)) {
1594
+ console.error("Component yielded more than once in for...of loop");
1595
+ }
1596
+ ctx.f &= -17;
1597
+ if (iteration.done) {
1598
+ ctx.f &= -257;
1599
+ ctx.iterator = undefined;
1600
+ }
1601
+ let value;
1602
+ try {
1603
+ value = updateComponentChildren(ctx,
1604
+ // Children can be void so we eliminate that here
1605
+ iteration.value, hydrationData);
1606
+ if (isPromiseLike(value)) {
1607
+ value = value.catch((err) => handleChildError(ctx, err));
1608
+ }
1609
+ }
1610
+ catch (err) {
1611
+ value = handleChildError(ctx, err);
1612
+ }
1613
+ const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1614
+ return [block, value];
1615
+ }
1616
+ else {
1617
+ if (ctx.f & IsInForOfLoop) {
1618
+ // Async generator component using for...of loops behave similar to sync
1619
+ // generator components. This allows for easier refactoring of sync to
1620
+ // async generator components.
1621
+ if (!initial) {
1622
+ try {
1623
+ ctx.f |= IsSyncExecuting;
1624
+ iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1625
+ }
1626
+ catch (err) {
1627
+ ctx.f |= IsErrored;
1628
+ throw err;
1629
+ }
1630
+ finally {
1631
+ ctx.f &= -3;
1632
+ }
1633
+ }
1634
+ if (!isPromiseLike(iteration)) {
1635
+ throw new Error("Mixed generator component");
1636
+ }
1637
+ const block = iteration.catch(NOOP);
1638
+ const value = iteration.then((iteration) => {
1639
+ let value;
1640
+ if (!(ctx.f & IsInForOfLoop)) {
1641
+ runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
1642
+ }
1643
+ else {
1644
+ if (!(ctx.f & NeedsToYield) && !(ctx.f & IsUnmounted)) {
1645
+ console.error("Component yielded more than once in for...of loop");
1646
+ }
1647
+ }
1648
+ ctx.f &= -17;
1649
+ try {
1650
+ value = updateComponentChildren(ctx,
1651
+ // Children can be void so we eliminate that here
1652
+ iteration.value, hydrationData);
1653
+ if (isPromiseLike(value)) {
1654
+ value = value.catch((err) => handleChildError(ctx, err));
1655
+ }
1656
+ }
1657
+ catch (err) {
1658
+ value = handleChildError(ctx, err);
1659
+ }
1660
+ return value;
1661
+ }, (err) => {
1662
+ ctx.f |= IsErrored;
1663
+ throw err;
1664
+ });
1665
+ return [block, value];
1666
+ }
1667
+ else {
1668
+ runAsyncGenComponent(ctx, iteration, hydrationData, initial);
1669
+ return [ctx.inflightBlock, ctx.inflightValue];
1670
+ }
1671
+ }
1672
+ }
1673
+ async function runAsyncGenComponent(ctx, iterationP, hydrationData, initial = false) {
1674
+ let done = false;
1675
+ try {
1676
+ while (!done) {
1677
+ if (ctx.f & IsInForOfLoop) {
1678
+ break;
1679
+ }
1680
+ // inflightValue must be set synchronously.
1681
+ let onValue;
1682
+ ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1683
+ if (ctx.f & IsUpdating) {
1684
+ // We should not swallow unhandled promise rejections if the component is
1685
+ // updating independently.
1686
+ // TODO: Does this handle this.refresh() calls?
1687
+ ctx.inflightValue.catch(NOOP);
1688
+ }
1689
+ let iteration;
1690
+ try {
1691
+ iteration = await iterationP;
1692
+ }
1693
+ catch (err) {
1694
+ done = true;
1695
+ ctx.f |= IsErrored;
1696
+ onValue(Promise.reject(err));
1697
+ break;
1698
+ }
1699
+ if (!(ctx.f & IsInForAwaitOfLoop)) {
1700
+ ctx.f &= ~PropsAvailable;
1701
+ }
1702
+ done = !!iteration.done;
1703
+ let value;
1704
+ try {
1705
+ if (!(ctx.f & NeedsToYield) &&
1706
+ ctx.f & PropsAvailable &&
1707
+ ctx.f & IsInForAwaitOfLoop &&
1708
+ !initial &&
1709
+ !done) {
1710
+ // We skip stale iterations in for await...of loops.
1711
+ value = ctx.ret.inflightValue || getValue(ctx.ret);
1712
+ }
1713
+ else {
1714
+ value = updateComponentChildren(ctx, iteration.value, hydrationData);
1715
+ hydrationData = undefined;
1716
+ if (isPromiseLike(value)) {
1717
+ value = value.catch((err) => handleChildError(ctx, err));
1718
+ }
1719
+ }
1720
+ ctx.f &= ~NeedsToYield;
1721
+ }
1722
+ catch (err) {
1723
+ // Do we need to catch potential errors here in the case of unhandled
1724
+ // promise rejections?
1725
+ value = handleChildError(ctx, err);
1726
+ }
1727
+ finally {
1728
+ onValue(value);
1729
+ }
1730
+ let oldResult;
1731
+ if (ctx.ret.inflightValue) {
1732
+ // The value passed back into the generator as the argument to the next
1733
+ // method is a promise if an async generator component has async
1734
+ // children. Sync generator components only resume when their children
1735
+ // have fulfilled so the element’s inflight child values will never be
1736
+ // defined.
1737
+ oldResult = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value));
1738
+ oldResult.catch((err) => {
1739
+ if (ctx.f & IsUpdating) {
1740
+ return;
1741
+ }
1742
+ if (!ctx.parent) {
1743
+ throw err;
1744
+ }
1745
+ return propagateError(ctx.parent, err);
1746
+ });
1747
+ }
1748
+ else {
1749
+ oldResult = ctx.renderer.read(getValue(ctx.ret));
1750
+ }
1751
+ if (ctx.f & IsUnmounted) {
1752
+ if (ctx.f & IsInForAwaitOfLoop) {
1753
+ try {
1754
+ ctx.f |= IsSyncExecuting;
1755
+ iterationP = ctx.iterator.next(oldResult);
1756
+ }
1757
+ finally {
1758
+ ctx.f &= ~IsSyncExecuting;
1759
+ }
1760
+ }
1761
+ else {
1762
+ returnComponent(ctx);
1763
+ break;
1764
+ }
1765
+ }
1766
+ else if (!done && !(ctx.f & IsInForOfLoop)) {
1767
+ try {
1768
+ ctx.f |= IsSyncExecuting;
1769
+ iterationP = ctx.iterator.next(oldResult);
1770
+ }
1771
+ finally {
1772
+ ctx.f &= ~IsSyncExecuting;
1773
+ }
1774
+ }
1775
+ initial = false;
1776
+ }
1777
+ }
1778
+ finally {
1779
+ if (done) {
1780
+ ctx.f &= -513;
1781
+ ctx.iterator = undefined;
1782
+ }
1783
+ }
1784
+ }
1785
+ /**
1786
+ * Called to resume the props async iterator for async generator components.
1787
+ */
1788
+ function resumePropsAsyncIterator(ctx) {
1789
+ if (ctx.onProps) {
1790
+ ctx.onProps(ctx.ret.el.props);
1791
+ ctx.onProps = undefined;
1792
+ ctx.f &= -33;
1793
+ }
1794
+ else {
1795
+ ctx.f |= PropsAvailable;
1796
+ }
1797
+ }
1798
+ // TODO: async unmounting
1799
+ function unmountComponent(ctx) {
1800
+ if (ctx.f & IsUnmounted) {
1801
+ return;
1802
+ }
1803
+ clearEventListeners(ctx);
1804
+ const callbacks = cleanupMap.get(ctx);
1805
+ if (callbacks) {
1806
+ cleanupMap.delete(ctx);
1807
+ const value = ctx.renderer.read(getValue(ctx.ret));
1808
+ for (const callback of callbacks) {
1809
+ callback(value);
1810
+ }
1811
+ }
1812
+ ctx.f |= IsUnmounted;
1813
+ if (ctx.iterator) {
1814
+ if (ctx.f & IsSyncGen) {
1815
+ let value;
1816
+ if (ctx.f & IsInForOfLoop) {
1817
+ value = enqueueComponentRun(ctx);
1818
+ }
1819
+ if (isPromiseLike(value)) {
1820
+ value.then(() => {
1821
+ if (ctx.f & IsInForOfLoop) {
1822
+ unmountComponent(ctx);
1823
+ }
1824
+ else {
1825
+ returnComponent(ctx);
1826
+ }
1827
+ }, (err) => {
1828
+ if (!ctx.parent) {
1829
+ throw err;
1830
+ }
1831
+ return propagateError(ctx.parent, err);
1832
+ });
1833
+ }
1834
+ else {
1835
+ if (ctx.f & IsInForOfLoop) {
1836
+ unmountComponent(ctx);
1837
+ }
1838
+ else {
1839
+ returnComponent(ctx);
1840
+ }
1841
+ }
1842
+ }
1843
+ else if (ctx.f & IsAsyncGen) {
1844
+ if (ctx.f & IsInForOfLoop) {
1845
+ const value = enqueueComponentRun(ctx);
1846
+ value.then(() => {
1847
+ if (ctx.f & IsInForOfLoop) {
1848
+ unmountComponent(ctx);
1849
+ }
1850
+ else {
1851
+ returnComponent(ctx);
1852
+ }
1853
+ }, (err) => {
1854
+ if (!ctx.parent) {
1855
+ throw err;
1856
+ }
1857
+ return propagateError(ctx.parent, err);
1858
+ });
1859
+ }
1860
+ else {
1861
+ // The logic for unmounting async generator components is in the
1862
+ // runAsyncGenComponent function.
1863
+ resumePropsAsyncIterator(ctx);
1864
+ }
1865
+ }
1866
+ }
1867
+ }
1868
+ function returnComponent(ctx) {
1869
+ resumePropsAsyncIterator(ctx);
1870
+ if (ctx.iterator && typeof ctx.iterator.return === "function") {
1871
+ try {
1872
+ ctx.f |= IsSyncExecuting;
1873
+ const iteration = ctx.iterator.return();
1874
+ if (isPromiseLike(iteration)) {
1875
+ iteration.catch((err) => {
1876
+ if (!ctx.parent) {
1877
+ throw err;
1878
+ }
1879
+ return propagateError(ctx.parent, err);
1880
+ });
1881
+ }
1882
+ }
1883
+ finally {
1884
+ ctx.f &= -3;
1885
+ }
1886
+ }
1887
+ }
1888
+ /*** EVENT TARGET UTILITIES ***/
1889
+ // EVENT PHASE CONSTANTS
1890
+ // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
1891
+ const NONE = 0;
1892
+ const CAPTURING_PHASE = 1;
1893
+ const AT_TARGET = 2;
1894
+ const BUBBLING_PHASE = 3;
1895
+ const listenersMap = new WeakMap();
1896
+ function isListenerOrListenerObject(value) {
1897
+ return (typeof value === "function" ||
1898
+ (value !== null &&
1899
+ typeof value === "object" &&
1900
+ typeof value.handleEvent === "function"));
1901
+ }
1902
+ function normalizeListenerOptions(options) {
1903
+ if (typeof options === "boolean") {
1904
+ return { capture: options };
1905
+ }
1906
+ else if (options == null) {
1907
+ return {};
1908
+ }
1909
+ return options;
1910
+ }
1911
+ function isEventTarget(value) {
1912
+ return (value != null &&
1913
+ typeof value.addEventListener === "function" &&
1914
+ typeof value.removeEventListener === "function" &&
1915
+ typeof value.dispatchEvent === "function");
1916
+ }
1917
+ function setEventProperty(ev, key, value) {
1918
+ Object.defineProperty(ev, key, { value, writable: false, configurable: true });
1919
+ }
1920
+ // TODO: Maybe we can pass in the current context directly, rather than
1921
+ // starting from the parent?
1922
+ /**
1923
+ * A function to reconstruct an array of every listener given a context and a
1924
+ * host element.
1925
+ *
1926
+ * This function exploits the fact that contexts retain their nearest ancestor
1927
+ * host element. We can determine all the contexts which are directly listening
1928
+ * to an element by traversing up the context tree and checking that the host
1929
+ * element passed in matches the parent context’s host element.
1930
+ */
1931
+ function getListenerRecords(ctx, ret) {
1932
+ let listeners = [];
1933
+ while (ctx !== undefined && ctx.host === ret) {
1934
+ const listeners1 = listenersMap.get(ctx);
1935
+ if (listeners1) {
1936
+ listeners = listeners.concat(listeners1);
1937
+ }
1938
+ ctx = ctx.parent;
1939
+ }
1940
+ return listeners;
1941
+ }
1942
+ function clearEventListeners(ctx) {
1943
+ const listeners = listenersMap.get(ctx);
1944
+ if (listeners && listeners.length) {
1945
+ for (const value of getChildValues(ctx.ret)) {
1946
+ if (isEventTarget(value)) {
1947
+ for (const record of listeners) {
1948
+ value.removeEventListener(record.type, record.callback, record.options);
1949
+ }
1950
+ }
1951
+ }
1952
+ listeners.length = 0;
1953
+ }
1954
+ }
1955
+ /*** ERROR HANDLING UTILITIES ***/
1956
+ function handleChildError(ctx, err) {
1957
+ if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
1958
+ throw err;
1959
+ }
1960
+ resumePropsAsyncIterator(ctx);
1961
+ let iteration;
1962
+ try {
1963
+ ctx.f |= IsSyncExecuting;
1964
+ iteration = ctx.iterator.throw(err);
1965
+ }
1966
+ catch (err) {
1967
+ ctx.f |= IsErrored;
1968
+ throw err;
1969
+ }
1970
+ finally {
1971
+ ctx.f &= -3;
1972
+ }
1973
+ if (isPromiseLike(iteration)) {
1974
+ return iteration.then((iteration) => {
1975
+ if (iteration.done) {
1976
+ ctx.f &= -513;
1977
+ ctx.iterator = undefined;
1978
+ }
1979
+ return updateComponentChildren(ctx, iteration.value);
1980
+ }, (err) => {
1981
+ ctx.f |= IsErrored;
1982
+ throw err;
1983
+ });
1984
+ }
1985
+ if (iteration.done) {
1986
+ ctx.f &= -257;
1987
+ ctx.f &= -513;
1988
+ ctx.iterator = undefined;
1989
+ }
1990
+ return updateComponentChildren(ctx, iteration.value);
1991
+ }
1992
+ function propagateError(ctx, err) {
1993
+ let result;
1994
+ try {
1995
+ result = handleChildError(ctx, err);
1996
+ }
1997
+ catch (err) {
1998
+ if (!ctx.parent) {
1999
+ throw err;
2000
+ }
2001
+ return propagateError(ctx.parent, err);
2002
+ }
2003
+ if (isPromiseLike(result)) {
2004
+ return result.catch((err) => {
2005
+ if (!ctx.parent) {
2006
+ throw err;
2007
+ }
2008
+ return propagateError(ctx.parent, err);
2009
+ });
2010
+ }
2011
+ return result;
1988
2012
  }
1989
2013
 
1990
- const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1991
- const impl$1 = {
1992
- scope(xmlns, tag, props) {
1993
- switch (tag) {
1994
- case Portal:
1995
- xmlns = undefined;
1996
- break;
1997
- case "svg":
1998
- xmlns = SVG_NAMESPACE;
1999
- break;
2000
- }
2001
- return props.xmlns || xmlns;
2002
- },
2003
- create(tag, _props, xmlns) {
2004
- if (typeof tag !== "string") {
2005
- throw new Error(`Unknown tag: ${tag.toString()}`);
2006
- }
2007
- else if (tag.toLowerCase() === "svg") {
2008
- xmlns = SVG_NAMESPACE;
2009
- }
2010
- return xmlns
2011
- ? document.createElementNS(xmlns, tag)
2012
- : document.createElement(tag);
2013
- },
2014
- hydrate(tag, node, props) {
2015
- if (typeof tag !== "string" && tag !== Portal) {
2016
- throw new Error(`Unknown tag: ${tag.toString()}`);
2017
- }
2018
- if (typeof tag === "string" &&
2019
- tag.toUpperCase() !== node.tagName) {
2020
- // TODO: consider pros and cons of hydration warnings
2021
- //console.error(`Expected <${tag}> while hydrating but found:`, node);
2022
- return undefined;
2023
- }
2024
- const children = [];
2025
- for (let i = 0; i < node.childNodes.length; i++) {
2026
- const child = node.childNodes[i];
2027
- if (child.nodeType === Node.TEXT_NODE) {
2028
- children.push(child.data);
2029
- }
2030
- else if (child.nodeType === Node.ELEMENT_NODE) {
2031
- children.push(child);
2032
- }
2033
- }
2034
- // TODO: extract props from nodes
2035
- return { props, children };
2036
- },
2037
- patch(_tag,
2038
- // TODO: Why does this assignment work?
2039
- node, name,
2040
- // TODO: Stricter typings?
2041
- value, oldValue, xmlns) {
2042
- const isSVG = xmlns === SVG_NAMESPACE;
2043
- switch (name) {
2044
- case "style": {
2045
- const style = node.style;
2046
- if (style == null) {
2047
- node.setAttribute("style", value);
2048
- }
2049
- else if (value == null || value === false) {
2050
- node.removeAttribute("style");
2051
- }
2052
- else if (value === true) {
2053
- node.setAttribute("style", "");
2054
- }
2055
- else if (typeof value === "string") {
2056
- if (style.cssText !== value) {
2057
- style.cssText = value;
2058
- }
2059
- }
2060
- else {
2061
- if (typeof oldValue === "string") {
2062
- style.cssText = "";
2063
- }
2064
- for (const styleName in { ...oldValue, ...value }) {
2065
- const styleValue = value && value[styleName];
2066
- if (styleValue == null) {
2067
- style.removeProperty(styleName);
2068
- }
2069
- else if (style.getPropertyValue(styleName) !== styleValue) {
2070
- style.setProperty(styleName, styleValue);
2071
- }
2072
- }
2073
- }
2074
- break;
2075
- }
2076
- case "class":
2077
- case "className":
2078
- if (value === true) {
2079
- node.setAttribute("class", "");
2080
- }
2081
- else if (value == null) {
2082
- node.removeAttribute("class");
2083
- }
2084
- else if (!isSVG) {
2085
- if (node.className !== value) {
2086
- node["className"] = value;
2087
- }
2088
- }
2089
- else if (node.getAttribute("class") !== value) {
2090
- node.setAttribute("class", value);
2091
- }
2092
- break;
2093
- case "innerHTML":
2094
- if (value !== oldValue) {
2095
- node.innerHTML = value;
2096
- }
2097
- break;
2098
- default: {
2099
- if (name in node &&
2100
- // boolean properties will coerce strings, but sometimes they map to
2101
- // enumerated attributes, where truthy strings ("false", "no") map to
2102
- // falsy properties, so we use attributes in this case.
2103
- !(typeof value === "string" &&
2104
- typeof node[name] === "boolean")) {
2105
- // walk up the object's prototype chain to find the owner of the
2106
- // named property
2107
- let obj = node;
2108
- do {
2109
- if (Object.prototype.hasOwnProperty.call(obj, name)) {
2110
- break;
2111
- }
2112
- } while ((obj = Object.getPrototypeOf(obj)));
2113
- // get the descriptor for the named property and check whether it
2114
- // implies that the property is writable
2115
- const descriptor = Object.getOwnPropertyDescriptor(obj, name);
2116
- if (descriptor != null &&
2117
- (descriptor.writable === true || descriptor.set !== undefined)) {
2118
- if (node[name] !== value || oldValue === undefined) {
2119
- node[name] = value;
2120
- }
2121
- return;
2122
- }
2123
- // if the property wasn't writable, fall through to the code below
2124
- // which uses setAttribute() instead of assigning directly.
2125
- }
2126
- if (value === true) {
2127
- value = "";
2128
- }
2129
- else if (value == null || value === false) {
2130
- node.removeAttribute(name);
2131
- return;
2132
- }
2133
- if (node.getAttribute(name) !== value) {
2134
- node.setAttribute(name, value);
2135
- }
2136
- }
2137
- }
2138
- },
2139
- arrange(tag, node, props, children, _oldProps, oldChildren) {
2140
- if (tag === Portal && (node == null || typeof node.nodeType !== "number")) {
2141
- throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`);
2142
- }
2143
- if (!("innerHTML" in props) &&
2144
- // We don’t want to update elements without explicit children (<div/>),
2145
- // because these elements sometimes have child nodes added via raw
2146
- // DOM manipulations.
2147
- // However, if an element has previously rendered children, we clear the
2148
- // them because it would be surprising not to clear Crank managed
2149
- // children, even if the new element does not have explicit children.
2150
- ("children" in props || (oldChildren && oldChildren.length))) {
2151
- if (children.length === 0) {
2152
- node.textContent = "";
2153
- }
2154
- else {
2155
- let oldChild = node.firstChild;
2156
- let i = 0;
2157
- while (oldChild !== null && i < children.length) {
2158
- const newChild = children[i];
2159
- if (oldChild === newChild) {
2160
- oldChild = oldChild.nextSibling;
2161
- i++;
2162
- }
2163
- else if (typeof newChild === "string") {
2164
- if (oldChild.nodeType === Node.TEXT_NODE) {
2165
- if (oldChild.data !== newChild) {
2166
- oldChild.data = newChild;
2167
- }
2168
- oldChild = oldChild.nextSibling;
2169
- }
2170
- else {
2171
- node.insertBefore(document.createTextNode(newChild), oldChild);
2172
- }
2173
- i++;
2174
- }
2175
- else if (oldChild.nodeType === Node.TEXT_NODE) {
2176
- const nextSibling = oldChild.nextSibling;
2177
- node.removeChild(oldChild);
2178
- oldChild = nextSibling;
2179
- }
2180
- else {
2181
- node.insertBefore(newChild, oldChild);
2182
- i++;
2183
- // TODO: This is an optimization but we need to think a little more about other cases like prepending.
2184
- if (oldChild !== children[i]) {
2185
- const nextSibling = oldChild.nextSibling;
2186
- node.removeChild(oldChild);
2187
- oldChild = nextSibling;
2188
- }
2189
- }
2190
- }
2191
- // remove excess DOM nodes
2192
- while (oldChild !== null) {
2193
- const nextSibling = oldChild.nextSibling;
2194
- node.removeChild(oldChild);
2195
- oldChild = nextSibling;
2196
- }
2197
- // append excess children
2198
- for (; i < children.length; i++) {
2199
- const newChild = children[i];
2200
- node.appendChild(typeof newChild === "string"
2201
- ? document.createTextNode(newChild)
2202
- : newChild);
2203
- }
2204
- }
2205
- }
2206
- },
2207
- text(text, _scope, hydrationData) {
2208
- if (hydrationData != null) {
2209
- let value = hydrationData.children.shift();
2210
- if (typeof value !== "string" || !value.startsWith(text)) ;
2211
- else if (text.length < value.length) {
2212
- value = value.slice(text.length);
2213
- hydrationData.children.unshift(value);
2214
- }
2215
- }
2216
- return text;
2217
- },
2218
- raw(value, xmlns, hydrationData) {
2219
- let result;
2220
- if (typeof value === "string") {
2221
- const el = xmlns == null
2222
- ? document.createElement("div")
2223
- : document.createElementNS(xmlns, "svg");
2224
- el.innerHTML = value;
2225
- if (el.childNodes.length === 0) {
2226
- result = undefined;
2227
- }
2228
- else if (el.childNodes.length === 1) {
2229
- result = el.childNodes[0];
2230
- }
2231
- else {
2232
- result = Array.from(el.childNodes);
2233
- }
2234
- }
2235
- else {
2236
- result = value;
2237
- }
2238
- if (hydrationData != null) {
2239
- // TODO: maybe we should warn on incorrect values
2240
- if (Array.isArray(result)) {
2241
- for (let i = 0; i < result.length; i++) {
2242
- const node = result[i];
2243
- if (typeof node !== "string" &&
2244
- (node.nodeType === Node.ELEMENT_NODE ||
2245
- node.nodeType === Node.TEXT_NODE)) {
2246
- hydrationData.children.shift();
2247
- }
2248
- }
2249
- }
2250
- else if (result != null && typeof result !== "string") {
2251
- if (result.nodeType === Node.ELEMENT_NODE ||
2252
- result.nodeType === Node.TEXT_NODE) {
2253
- hydrationData.children.shift();
2254
- }
2255
- }
2256
- }
2257
- return result;
2258
- },
2259
- };
2260
- class DOMRenderer extends Renderer {
2261
- constructor() {
2262
- super(impl$1);
2263
- }
2264
- render(children, root, ctx) {
2265
- validateRoot(root);
2266
- return super.render(children, root, ctx);
2267
- }
2268
- hydrate(children, root, ctx) {
2269
- validateRoot(root);
2270
- return super.hydrate(children, root, ctx);
2271
- }
2272
- }
2273
- function validateRoot(root) {
2274
- if (root === null ||
2275
- (typeof root === "object" && typeof root.nodeType !== "number")) {
2276
- throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
2277
- }
2278
- }
2014
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2015
+ const impl$1 = {
2016
+ scope(xmlns, tag, props) {
2017
+ switch (tag) {
2018
+ case Portal:
2019
+ xmlns = undefined;
2020
+ break;
2021
+ case "svg":
2022
+ xmlns = SVG_NAMESPACE;
2023
+ break;
2024
+ }
2025
+ return props.xmlns || xmlns;
2026
+ },
2027
+ create(tag, _props, xmlns) {
2028
+ if (typeof tag !== "string") {
2029
+ throw new Error(`Unknown tag: ${tag.toString()}`);
2030
+ }
2031
+ else if (tag.toLowerCase() === "svg") {
2032
+ xmlns = SVG_NAMESPACE;
2033
+ }
2034
+ return xmlns
2035
+ ? document.createElementNS(xmlns, tag)
2036
+ : document.createElement(tag);
2037
+ },
2038
+ hydrate(tag, node, props) {
2039
+ if (typeof tag !== "string" && tag !== Portal) {
2040
+ throw new Error(`Unknown tag: ${tag.toString()}`);
2041
+ }
2042
+ if (typeof tag === "string" &&
2043
+ tag.toUpperCase() !== node.tagName) {
2044
+ // TODO: consider pros and cons of hydration warnings
2045
+ //console.error(`Expected <${tag}> while hydrating but found:`, node);
2046
+ return undefined;
2047
+ }
2048
+ const children = [];
2049
+ for (let i = 0; i < node.childNodes.length; i++) {
2050
+ const child = node.childNodes[i];
2051
+ if (child.nodeType === Node.TEXT_NODE) {
2052
+ children.push(child.data);
2053
+ }
2054
+ else if (child.nodeType === Node.ELEMENT_NODE) {
2055
+ children.push(child);
2056
+ }
2057
+ }
2058
+ // TODO: extract props from nodes
2059
+ return { props, children };
2060
+ },
2061
+ patch(_tag,
2062
+ // TODO: Why does this assignment work?
2063
+ node, name,
2064
+ // TODO: Stricter typings?
2065
+ value, oldValue, xmlns) {
2066
+ const isSVG = xmlns === SVG_NAMESPACE;
2067
+ const colonIndex = name.indexOf(":");
2068
+ if (colonIndex !== -1) {
2069
+ const ns = name.slice(0, colonIndex);
2070
+ const name1 = name.slice(colonIndex + 1);
2071
+ switch (ns) {
2072
+ case "prop":
2073
+ node[name1] = value;
2074
+ return;
2075
+ case "attr":
2076
+ if (value == null || value === false) {
2077
+ node.removeAttribute(name1);
2078
+ }
2079
+ else if (value === true) {
2080
+ node.setAttribute(name1, "");
2081
+ }
2082
+ else if (typeof value === "string") {
2083
+ node.setAttribute(name1, value);
2084
+ }
2085
+ else {
2086
+ node.setAttribute(name, String(value));
2087
+ }
2088
+ return;
2089
+ }
2090
+ }
2091
+ switch (name) {
2092
+ case "style": {
2093
+ const style = node.style;
2094
+ if (style == null) {
2095
+ node.setAttribute("style", value);
2096
+ }
2097
+ else if (value == null || value === false) {
2098
+ node.removeAttribute("style");
2099
+ }
2100
+ else if (value === true) {
2101
+ node.setAttribute("style", "");
2102
+ }
2103
+ else if (typeof value === "string") {
2104
+ if (style.cssText !== value) {
2105
+ style.cssText = value;
2106
+ }
2107
+ }
2108
+ else {
2109
+ if (typeof oldValue === "string") {
2110
+ style.cssText = "";
2111
+ }
2112
+ for (const styleName in { ...oldValue, ...value }) {
2113
+ const styleValue = value && value[styleName];
2114
+ if (styleValue == null) {
2115
+ style.removeProperty(styleName);
2116
+ }
2117
+ else if (style.getPropertyValue(styleName) !== styleValue) {
2118
+ style.setProperty(styleName, styleValue);
2119
+ }
2120
+ }
2121
+ }
2122
+ break;
2123
+ }
2124
+ case "class":
2125
+ case "className":
2126
+ if (value === true) {
2127
+ node.setAttribute("class", "");
2128
+ }
2129
+ else if (value == null) {
2130
+ node.removeAttribute("class");
2131
+ }
2132
+ else if (!isSVG) {
2133
+ if (node.className !== value) {
2134
+ node["className"] = value;
2135
+ }
2136
+ }
2137
+ else if (node.getAttribute("class") !== value) {
2138
+ node.setAttribute("class", value);
2139
+ }
2140
+ break;
2141
+ case "innerHTML":
2142
+ if (value !== oldValue) {
2143
+ node.innerHTML = value;
2144
+ }
2145
+ break;
2146
+ default: {
2147
+ if (name[0] === "o" &&
2148
+ name[1] === "n" &&
2149
+ name[2] === name[2].toUpperCase() &&
2150
+ typeof value === "function") {
2151
+ // Support React-style event names (onClick, onChange, etc.)
2152
+ name = name.toLowerCase();
2153
+ }
2154
+ if (name in node &&
2155
+ // boolean properties will coerce strings, but sometimes they map to
2156
+ // enumerated attributes, where truthy strings ("false", "no") map to
2157
+ // falsy properties, so we use attributes in this case.
2158
+ !(typeof value === "string" &&
2159
+ typeof node[name] === "boolean")) {
2160
+ // walk up the object's prototype chain to find the owner of the
2161
+ // named property
2162
+ let obj = node;
2163
+ do {
2164
+ if (Object.prototype.hasOwnProperty.call(obj, name)) {
2165
+ break;
2166
+ }
2167
+ } while ((obj = Object.getPrototypeOf(obj)));
2168
+ // get the descriptor for the named property and check whether it
2169
+ // implies that the property is writable
2170
+ const descriptor = Object.getOwnPropertyDescriptor(obj, name);
2171
+ if (descriptor != null &&
2172
+ (descriptor.writable === true || descriptor.set !== undefined)) {
2173
+ if (node[name] !== value || oldValue === undefined) {
2174
+ node[name] = value;
2175
+ }
2176
+ return;
2177
+ }
2178
+ // if the property wasn't writable, fall through to the code below
2179
+ // which uses setAttribute() instead of assigning directly.
2180
+ }
2181
+ if (value === true) {
2182
+ value = "";
2183
+ }
2184
+ else if (value == null || value === false) {
2185
+ node.removeAttribute(name);
2186
+ return;
2187
+ }
2188
+ if (node.getAttribute(name) !== value) {
2189
+ node.setAttribute(name, value);
2190
+ }
2191
+ }
2192
+ }
2193
+ },
2194
+ arrange(tag, node, props, children, _oldProps, oldChildren) {
2195
+ if (tag === Portal && (node == null || typeof node.nodeType !== "number")) {
2196
+ throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`);
2197
+ }
2198
+ if (!("innerHTML" in props) &&
2199
+ // We don’t want to update elements without explicit children (<div/>),
2200
+ // because these elements sometimes have child nodes added via raw
2201
+ // DOM manipulations.
2202
+ // However, if an element has previously rendered children, we clear the
2203
+ // them because it would be surprising not to clear Crank managed
2204
+ // children, even if the new element does not have explicit children.
2205
+ ("children" in props || (oldChildren && oldChildren.length))) {
2206
+ if (children.length === 0) {
2207
+ node.textContent = "";
2208
+ }
2209
+ else {
2210
+ let oldChild = node.firstChild;
2211
+ let i = 0;
2212
+ while (oldChild !== null && i < children.length) {
2213
+ const newChild = children[i];
2214
+ if (oldChild === newChild) {
2215
+ oldChild = oldChild.nextSibling;
2216
+ i++;
2217
+ }
2218
+ else if (typeof newChild === "string") {
2219
+ if (oldChild.nodeType === Node.TEXT_NODE) {
2220
+ if (oldChild.data !== newChild) {
2221
+ oldChild.data = newChild;
2222
+ }
2223
+ oldChild = oldChild.nextSibling;
2224
+ }
2225
+ else {
2226
+ node.insertBefore(document.createTextNode(newChild), oldChild);
2227
+ }
2228
+ i++;
2229
+ }
2230
+ else if (oldChild.nodeType === Node.TEXT_NODE) {
2231
+ const nextSibling = oldChild.nextSibling;
2232
+ node.removeChild(oldChild);
2233
+ oldChild = nextSibling;
2234
+ }
2235
+ else {
2236
+ node.insertBefore(newChild, oldChild);
2237
+ i++;
2238
+ // TODO: This is an optimization but we need to think a little more about other cases like prepending.
2239
+ if (oldChild !== children[i]) {
2240
+ const nextSibling = oldChild.nextSibling;
2241
+ node.removeChild(oldChild);
2242
+ oldChild = nextSibling;
2243
+ }
2244
+ }
2245
+ }
2246
+ // remove excess DOM nodes
2247
+ while (oldChild !== null) {
2248
+ const nextSibling = oldChild.nextSibling;
2249
+ node.removeChild(oldChild);
2250
+ oldChild = nextSibling;
2251
+ }
2252
+ // append excess children
2253
+ for (; i < children.length; i++) {
2254
+ const newChild = children[i];
2255
+ node.appendChild(typeof newChild === "string"
2256
+ ? document.createTextNode(newChild)
2257
+ : newChild);
2258
+ }
2259
+ }
2260
+ }
2261
+ },
2262
+ text(text, _scope, hydrationData) {
2263
+ if (hydrationData != null) {
2264
+ let value = hydrationData.children.shift();
2265
+ if (typeof value !== "string" || !value.startsWith(text)) ;
2266
+ else if (text.length < value.length) {
2267
+ value = value.slice(text.length);
2268
+ hydrationData.children.unshift(value);
2269
+ }
2270
+ }
2271
+ return text;
2272
+ },
2273
+ raw(value, xmlns, hydrationData) {
2274
+ let result;
2275
+ if (typeof value === "string") {
2276
+ const el = xmlns == null
2277
+ ? document.createElement("div")
2278
+ : document.createElementNS(xmlns, "svg");
2279
+ el.innerHTML = value;
2280
+ if (el.childNodes.length === 0) {
2281
+ result = undefined;
2282
+ }
2283
+ else if (el.childNodes.length === 1) {
2284
+ result = el.childNodes[0];
2285
+ }
2286
+ else {
2287
+ result = Array.from(el.childNodes);
2288
+ }
2289
+ }
2290
+ else {
2291
+ result = value;
2292
+ }
2293
+ if (hydrationData != null) {
2294
+ // TODO: maybe we should warn on incorrect values
2295
+ if (Array.isArray(result)) {
2296
+ for (let i = 0; i < result.length; i++) {
2297
+ const node = result[i];
2298
+ if (typeof node !== "string" &&
2299
+ (node.nodeType === Node.ELEMENT_NODE ||
2300
+ node.nodeType === Node.TEXT_NODE)) {
2301
+ hydrationData.children.shift();
2302
+ }
2303
+ }
2304
+ }
2305
+ else if (result != null && typeof result !== "string") {
2306
+ if (result.nodeType === Node.ELEMENT_NODE ||
2307
+ result.nodeType === Node.TEXT_NODE) {
2308
+ hydrationData.children.shift();
2309
+ }
2310
+ }
2311
+ }
2312
+ return result;
2313
+ },
2314
+ };
2315
+ class DOMRenderer extends Renderer {
2316
+ constructor() {
2317
+ super(impl$1);
2318
+ }
2319
+ render(children, root, ctx) {
2320
+ validateRoot(root);
2321
+ return super.render(children, root, ctx);
2322
+ }
2323
+ hydrate(children, root, ctx) {
2324
+ validateRoot(root);
2325
+ return super.hydrate(children, root, ctx);
2326
+ }
2327
+ }
2328
+ function validateRoot(root) {
2329
+ if (root === null ||
2330
+ (typeof root === "object" && typeof root.nodeType !== "number")) {
2331
+ throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
2332
+ }
2333
+ }
2279
2334
  const renderer$1 = new DOMRenderer();
2280
2335
 
2281
2336
  var dom = /*#__PURE__*/Object.freeze({
@@ -2285,142 +2340,160 @@
2285
2340
  renderer: renderer$1
2286
2341
  });
2287
2342
 
2288
- const voidTags = new Set([
2289
- "area",
2290
- "base",
2291
- "br",
2292
- "col",
2293
- "command",
2294
- "embed",
2295
- "hr",
2296
- "img",
2297
- "input",
2298
- "keygen",
2299
- "link",
2300
- "meta",
2301
- "param",
2302
- "source",
2303
- "track",
2304
- "wbr",
2305
- ]);
2306
- function escape(text) {
2307
- return text.replace(/[&<>"']/g, (match) => {
2308
- switch (match) {
2309
- case "&":
2310
- return "&amp;";
2311
- case "<":
2312
- return "&lt;";
2313
- case ">":
2314
- return "&gt;";
2315
- case '"':
2316
- return "&quot;";
2317
- case "'":
2318
- return "&#039;";
2319
- default:
2320
- return "";
2321
- }
2322
- });
2323
- }
2324
- function printStyleObject(style) {
2325
- const cssStrings = [];
2326
- for (const [name, value] of Object.entries(style)) {
2327
- if (value != null) {
2328
- cssStrings.push(`${name}:${value};`);
2329
- }
2330
- }
2331
- return cssStrings.join("");
2332
- }
2333
- function printAttrs(props) {
2334
- const attrs = [];
2335
- for (const [name, value] of Object.entries(props)) {
2336
- switch (true) {
2337
- case name === "children":
2338
- case name === "innerHTML":
2339
- break;
2340
- case name === "style": {
2341
- if (typeof value === "string") {
2342
- attrs.push(`style="${escape(value)}"`);
2343
- }
2344
- else if (typeof value === "object") {
2345
- attrs.push(`style="${escape(printStyleObject(value))}"`);
2346
- }
2347
- break;
2348
- }
2349
- case name === "className": {
2350
- if ("class" in props || typeof value !== "string") {
2351
- continue;
2352
- }
2353
- attrs.push(`class="${escape(value)}"`);
2354
- break;
2355
- }
2356
- case typeof value === "string":
2357
- attrs.push(`${escape(name)}="${escape(value)}"`);
2358
- break;
2359
- case typeof value === "number":
2360
- attrs.push(`${escape(name)}="${value}"`);
2361
- break;
2362
- case value === true:
2363
- attrs.push(`${escape(name)}`);
2364
- break;
2365
- }
2366
- }
2367
- return attrs.join(" ");
2368
- }
2369
- function join(children) {
2370
- let result = "";
2371
- for (let i = 0; i < children.length; i++) {
2372
- const child = children[i];
2373
- result += typeof child === "string" ? child : child.value;
2374
- }
2375
- return result;
2376
- }
2377
- const impl = {
2378
- create() {
2379
- return { value: "" };
2380
- },
2381
- text(text) {
2382
- return escape(text);
2383
- },
2384
- read(value) {
2385
- if (Array.isArray(value)) {
2386
- return join(value);
2387
- }
2388
- else if (typeof value === "undefined") {
2389
- return "";
2390
- }
2391
- else if (typeof value === "string") {
2392
- return value;
2393
- }
2394
- else {
2395
- return value.value;
2396
- }
2397
- },
2398
- arrange(tag, node, props, children) {
2399
- if (tag === Portal) {
2400
- return;
2401
- }
2402
- else if (typeof tag !== "string") {
2403
- throw new Error(`Unknown tag: ${tag.toString()}`);
2404
- }
2405
- const attrs = printAttrs(props);
2406
- const open = `<${tag}${attrs.length ? " " : ""}${attrs}>`;
2407
- let result;
2408
- if (voidTags.has(tag)) {
2409
- result = open;
2410
- }
2411
- else {
2412
- const close = `</${tag}>`;
2413
- const contents = "innerHTML" in props ? props["innerHTML"] : join(children);
2414
- result = `${open}${contents}${close}`;
2415
- }
2416
- node.value = result;
2417
- },
2418
- };
2419
- class HTMLRenderer extends Renderer {
2420
- constructor() {
2421
- super(impl);
2422
- }
2423
- }
2343
+ const voidTags = new Set([
2344
+ "area",
2345
+ "base",
2346
+ "br",
2347
+ "col",
2348
+ "command",
2349
+ "embed",
2350
+ "hr",
2351
+ "img",
2352
+ "input",
2353
+ "keygen",
2354
+ "link",
2355
+ "meta",
2356
+ "param",
2357
+ "source",
2358
+ "track",
2359
+ "wbr",
2360
+ ]);
2361
+ function escape(text) {
2362
+ return text.replace(/[&<>"']/g, (match) => {
2363
+ switch (match) {
2364
+ case "&":
2365
+ return "&amp;";
2366
+ case "<":
2367
+ return "&lt;";
2368
+ case ">":
2369
+ return "&gt;";
2370
+ case '"':
2371
+ return "&quot;";
2372
+ case "'":
2373
+ return "&#039;";
2374
+ default:
2375
+ return "";
2376
+ }
2377
+ });
2378
+ }
2379
+ function printStyleObject(style) {
2380
+ const cssStrings = [];
2381
+ for (const [name, value] of Object.entries(style)) {
2382
+ if (value != null) {
2383
+ cssStrings.push(`${name}:${value};`);
2384
+ }
2385
+ }
2386
+ return cssStrings.join("");
2387
+ }
2388
+ function printAttrs(props) {
2389
+ const attrs = [];
2390
+ for (let [name, value] of Object.entries(props)) {
2391
+ // TODO: Because printAttrs is called in arrange, special props are not filtered out.
2392
+ // This should be handled by the core library.
2393
+ if (name === "children" ||
2394
+ name === "innerHTML" ||
2395
+ name === "key" ||
2396
+ name === "ref" ||
2397
+ name === "copy" ||
2398
+ name.startsWith("prop:") ||
2399
+ // TODO: Remove deprecated special props
2400
+ name === "crank-key" ||
2401
+ name === "crank-ref" ||
2402
+ name === "crank-static" ||
2403
+ name === "c-key" ||
2404
+ name === "c-ref" ||
2405
+ name === "c-static" ||
2406
+ name === "$key" ||
2407
+ name === "$ref" ||
2408
+ name === "$static") {
2409
+ continue;
2410
+ }
2411
+ else if (name === "style") {
2412
+ if (typeof value === "string") {
2413
+ attrs.push(`style="${escape(value)}"`);
2414
+ }
2415
+ else if (typeof value === "object") {
2416
+ attrs.push(`style="${escape(printStyleObject(value))}"`);
2417
+ }
2418
+ }
2419
+ else if (name === "className") {
2420
+ if ("class" in props || typeof value !== "string") {
2421
+ continue;
2422
+ }
2423
+ attrs.push(`class="${escape(value)}"`);
2424
+ }
2425
+ else {
2426
+ if (name.startsWith("attr:")) {
2427
+ name = name.slice("attr:".length);
2428
+ }
2429
+ if (typeof value === "string") {
2430
+ attrs.push(`${escape(name)}="${escape(value)}"`);
2431
+ }
2432
+ else if (typeof value === "number") {
2433
+ attrs.push(`${escape(name)}="${value}"`);
2434
+ }
2435
+ else if (value === true) {
2436
+ attrs.push(`${escape(name)}`);
2437
+ }
2438
+ }
2439
+ }
2440
+ return attrs.join(" ");
2441
+ }
2442
+ function join(children) {
2443
+ let result = "";
2444
+ for (let i = 0; i < children.length; i++) {
2445
+ const child = children[i];
2446
+ result += typeof child === "string" ? child : child.value;
2447
+ }
2448
+ return result;
2449
+ }
2450
+ const impl = {
2451
+ create() {
2452
+ return { value: "" };
2453
+ },
2454
+ text(text) {
2455
+ return escape(text);
2456
+ },
2457
+ read(value) {
2458
+ if (Array.isArray(value)) {
2459
+ return join(value);
2460
+ }
2461
+ else if (typeof value === "undefined") {
2462
+ return "";
2463
+ }
2464
+ else if (typeof value === "string") {
2465
+ return value;
2466
+ }
2467
+ else {
2468
+ return value.value;
2469
+ }
2470
+ },
2471
+ arrange(tag, node, props, children) {
2472
+ if (tag === Portal) {
2473
+ return;
2474
+ }
2475
+ else if (typeof tag !== "string") {
2476
+ throw new Error(`Unknown tag: ${tag.toString()}`);
2477
+ }
2478
+ const attrs = printAttrs(props);
2479
+ const open = `<${tag}${attrs.length ? " " : ""}${attrs}>`;
2480
+ let result;
2481
+ if (voidTags.has(tag)) {
2482
+ result = open;
2483
+ }
2484
+ else {
2485
+ const close = `</${tag}>`;
2486
+ const contents = "innerHTML" in props ? props["innerHTML"] : join(children);
2487
+ result = `${open}${contents}${close}`;
2488
+ }
2489
+ node.value = result;
2490
+ },
2491
+ };
2492
+ class HTMLRenderer extends Renderer {
2493
+ constructor() {
2494
+ super(impl);
2495
+ }
2496
+ }
2424
2497
  const renderer = new HTMLRenderer();
2425
2498
 
2426
2499
  var html = /*#__PURE__*/Object.freeze({