ilha 0.15.1 → 0.16.0

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.
@@ -1,1659 +0,0 @@
1
- import { S as toError, _ as isString, a as ISLAND, b as asFailure, c as KEEP, f as errorView, g as isObject, h as isNumber, i as Fragment, l as KEY_ATTR, m as isFunction, n as isSetupFn, o as ISLAND_MOUNT_INTERNAL, p as isBigInt, r as isVNode, s as ISLAND_SLOT_TAG, u as SLOT_ATTR, v as skip, x as failureMessage, y as unwrap } from "./vnode-BHUY61Rm.js";
2
- import * as Atom from "effect/reactivity/Atom";
3
- import { Equal } from "effect";
4
- import * as Effect$1 from "effect/Effect";
5
- import * as Stream$1 from "effect/Stream";
6
- import * as Cause from "effect/Cause";
7
- import * as Exit from "effect/Exit";
8
- import * as Fiber from "effect/Fiber";
9
- import * as Registry from "effect/reactivity/AtomRegistry";
10
- import * as Scope from "effect/Scope";
11
-
12
- //#region src/runtime.ts
13
- const stack = [];
14
- const getFiber = () => {
15
- const f = stack.findLast((x) => !x.closed);
16
- if (!f) throw new Error("ilha: no fiber");
17
- return f;
18
- };
19
- const getActiveFiber = () => stack.findLast((x) => !x.closed);
20
- const withFiber = (fiber, fn) => {
21
- stack.push(fiber);
22
- try {
23
- return fn();
24
- } finally {
25
- stack.pop();
26
- }
27
- };
28
- const provide = (effect, registry) => effect.pipe(Effect$1.provideService(Registry.AtomRegistry, registry));
29
- const runInScope = (effect, registry, scope, onOk, onErr, onDone) => {
30
- const provided = provide(effect, registry);
31
- const f = Effect$1.runFork(onOk || onDone ? provided.pipe(Effect$1.onExit((ex) => Effect$1.sync(() => {
32
- onDone?.();
33
- if (Exit.isSuccess(ex)) onOk?.(ex.value);
34
- else if (!Cause.hasInterruptsOnly(ex.cause)) onErr?.(Cause.squash(ex.cause));
35
- }))) : provided);
36
- Effect$1.runSync(Scope.addFinalizer(scope, Fiber.interrupt(f)));
37
- };
38
- /** Clear render subscriptions and child holes without closing the fiber scope. */
39
- const clearFiberView = (fiber) => {
40
- fiber.renderSub?.();
41
- fiber.renderSub = void 0;
42
- for (const slot of fiber.watchSlots ?? []) slot?.dispose();
43
- fiber.watchSlots = [];
44
- fiber.watchI = 0;
45
- fiber.primitives = void 0;
46
- fiber.primitiveI = 0;
47
- for (const release of fiber.primitiveHolds ?? []) release();
48
- fiber.primitiveHolds = void 0;
49
- fiber.islandFrame = void 0;
50
- fiber.keyedHoles = void 0;
51
- for (const h of fiber.holes) h.dispose();
52
- fiber.holes = [];
53
- };
54
- const closeFiber = (fiber) => {
55
- if (fiber.closed) return;
56
- fiber.closed = true;
57
- fiber.trackRestore?.();
58
- fiber.trackRestore = void 0;
59
- fiber.handleError = void 0;
60
- clearFiberView(fiber);
61
- Effect$1.runFork(Scope.close(fiber.scope, Exit.void));
62
- };
63
- const makeRuntime = (opts) => {
64
- const registry = Registry.make();
65
- const scope = Scope.makeUnsafe();
66
- let holeSeq = 0;
67
- let pending = 0;
68
- let onIdle;
69
- const runtime = {
70
- begin: () => {
71
- pending += 1;
72
- },
73
- close: () => {
74
- onIdle = void 0;
75
- Effect$1.runFork(Scope.close(scope, Exit.void));
76
- registry.dispose();
77
- },
78
- end: () => {
79
- pending -= 1;
80
- if (pending === 0) onIdle?.();
81
- },
82
- hydrateI: 0,
83
- hydrateValues: opts?.hydrate,
84
- later: (fn) => {
85
- runtime.begin();
86
- queueMicrotask(() => {
87
- try {
88
- fn();
89
- } finally {
90
- runtime.end();
91
- }
92
- });
93
- },
94
- nextHole: () => {
95
- holeSeq += 1;
96
- return holeSeq;
97
- },
98
- onError: opts?.onError,
99
- registry,
100
- scope,
101
- setIdle: (onReady) => {
102
- onIdle = onReady;
103
- if (pending === 0) return onReady();
104
- },
105
- ssr: opts?.ssr === true,
106
- ssrActions: {},
107
- ssrCapture: opts?.ssrCapture === true,
108
- ssrEventI: 0,
109
- ssrValues: []
110
- };
111
- return runtime;
112
- };
113
- const makeFiber = (runtime, root, paint, opts) => {
114
- const scope = Scope.forkUnsafe(runtime.scope);
115
- const fiber = {
116
- closed: false,
117
- fail: (e) => {
118
- opts?.onFail?.(e);
119
- },
120
- holes: [],
121
- inFlight: false,
122
- paint: (view) => paint(fiber, view),
123
- paintFn: paint,
124
- parent: opts?.parent,
125
- registry: runtime.registry,
126
- root,
127
- run: (effect, onOk, onErr) => {
128
- if (fiber.closed) return;
129
- if (onOk) fiber.runtime.begin();
130
- runInScope(effect, fiber.registry, fiber.scope, onOk && ((a) => {
131
- if (!fiber.closed) withFiber(fiber, () => onOk(a));
132
- }), onErr && ((e) => {
133
- if (!fiber.closed) withFiber(fiber, () => onErr(e));
134
- }), onOk && (() => fiber.runtime.end()));
135
- },
136
- runtime,
137
- scope
138
- };
139
- return fiber;
140
- };
141
- /** Walk this fiber and ancestors for an error boundary handler. */
142
- const reportFiberError = (fiber, error) => {
143
- let current = fiber;
144
- while (current) {
145
- const { handleError } = current;
146
- if (handleError) {
147
- handleError(error);
148
- return true;
149
- }
150
- current = current.parent;
151
- }
152
- return false;
153
- };
154
- /**
155
- * Open a mount's root fiber. A failing root component reports to
156
- * `runtime.onError`, paints the error view, and settles the mount.
157
- */
158
- const makeRootFiber = ({ runtime, root, paint, settle }) => {
159
- const fiber = makeFiber(runtime, root, paint, { onFail: (e) => {
160
- const error = toError(e);
161
- runtime.onError?.(error);
162
- console.error(error);
163
- fiber.paint(errorView(error));
164
- settle();
165
- } });
166
- return fiber;
167
- };
168
-
169
- //#endregion
170
- //#region src/atom.ts
171
- const handleOwner = /* @__PURE__ */ new WeakMap();
172
- const guardHandleFiber = (handle) => {
173
- const owner = handleOwner.get(handle);
174
- if (!owner) return;
175
- const active = getActiveFiber();
176
- if (!active || active.registry === owner.registry) return;
177
- throw new Error("ilha: atom handle used from a different island");
178
- };
179
- const instr = (effect) => {
180
- const fiber = getFiber();
181
- const out = {
182
- $$ilhaOp: 1,
183
- effect,
184
- *[Symbol.iterator]() {
185
- return yield out;
186
- }
187
- };
188
- const awaitInstruction = async (onFulfilled, onRejected) => {
189
- const { promise, resolve, reject } = Promise.withResolvers();
190
- fiber.run(out.effect, (a) => {
191
- withFiber(fiber, () => resolve(a));
192
- }, reject);
193
- try {
194
- const value = await promise;
195
- onFulfilled?.(value);
196
- } catch (error) {
197
- onRejected?.(error);
198
- }
199
- };
200
- Object.defineProperty(out, "then", { value: awaitInstruction });
201
- return out;
202
- };
203
- const isAtomHandle = (x) => isFunction(x) && x.$$atom === 1;
204
- /** Key-order-insensitive deep equality for JSON-shaped values. */
205
- const structuralEqual = Equal.equals;
206
- let trackGet;
207
- const trackStack = [];
208
- /** Read atoms inside `fn` without subscribing the active render. Sync only. */
209
- const untrack = (fn) => {
210
- const prev = trackGet;
211
- trackGet = void 0;
212
- try {
213
- return fn();
214
- } finally {
215
- trackGet = prev;
216
- }
217
- };
218
- const beginPrimitiveFrame = (fiber) => {
219
- const used = fiber.primitiveI ?? 0;
220
- const slots = fiber.primitives?.length ?? 0;
221
- if (used > 0 && used !== slots) console.warn(`ilha: atom/watch slot count changed between renders (${slots} → ${used}); keep primitive order stable`);
222
- fiber.primitiveI = 0;
223
- fiber.watchI = 0;
224
- if (!fiber.islandFrame) fiber.islandFrame = {
225
- i: 0,
226
- slots: []
227
- };
228
- const { islandFrame } = fiber;
229
- const usedIslands = islandFrame.i;
230
- if (usedIslands < islandFrame.slots.length) {
231
- for (let j = usedIslands; j < islandFrame.slots.length; j += 1) {
232
- const slot = islandFrame.slots[j];
233
- if (slot) {
234
- slot.unmount?.();
235
- islandFrame.slots[j] = void 0;
236
- }
237
- }
238
- islandFrame.slots.length = usedIslands;
239
- }
240
- islandFrame.i = 0;
241
- let { componentFrame } = fiber;
242
- if (!componentFrame) {
243
- componentFrame = {
244
- i: 0,
245
- slots: []
246
- };
247
- fiber.componentFrame = componentFrame;
248
- }
249
- const usedComponents = componentFrame.i;
250
- if (usedComponents < componentFrame.slots.length) {
251
- for (let j = usedComponents; j < componentFrame.slots.length; j += 1) {
252
- const slot = componentFrame.slots[j];
253
- if (slot) {
254
- const { hole } = slot;
255
- if (!hole.closed) {
256
- const idx = fiber.holes.findIndex((h) => h.holeFiber === hole);
257
- if (idx === -1) closeFiber(hole);
258
- else {
259
- const [removed] = fiber.holes.splice(idx, 1);
260
- removed?.dispose();
261
- }
262
- }
263
- }
264
- componentFrame.slots[j] = void 0;
265
- }
266
- componentFrame.slots.length = usedComponents;
267
- }
268
- componentFrame.i = 0;
269
- };
270
- const readTracked = (atom, registry) => {
271
- const frame = trackStack.at(-1);
272
- const active = getActiveFiber();
273
- if (trackGet && frame && (active === frame.fiber || active === void 0)) return trackGet(atom);
274
- return registry.get(atom);
275
- };
276
- const usePrimitiveSlot = (fiber, make) => {
277
- const i = fiber.primitiveI ?? 0;
278
- fiber.primitiveI = i + 1;
279
- fiber.primitives ??= [];
280
- const existing = fiber.primitives[i];
281
- if (existing) return {
282
- atom: existing,
283
- fresh: false
284
- };
285
- const a = make();
286
- fiber.primitives[i] = a;
287
- return {
288
- atom: a,
289
- fresh: true
290
- };
291
- };
292
- /**
293
- * Keep a component's own state atom in the registry while its fiber lives.
294
- *
295
- * The registry drops an atom nothing subscribes to. State that a handler
296
- * writes and the view never reads would otherwise reset between events.
297
- */
298
- const holdState = (fiber, atom) => {
299
- if (!fiber.runtime.ssr) {
300
- fiber.primitiveHolds ??= [];
301
- fiber.primitiveHolds.push(fiber.registry.mount(atom));
302
- }
303
- return atom;
304
- };
305
- const withTrackGetRun = (fiber, fn, onOk, onErr) => {
306
- const deps = /* @__PURE__ */ new Set();
307
- const get = (a) => {
308
- deps.add(a);
309
- return fiber.registry.get(a);
310
- };
311
- const restore = () => {
312
- if (fiber.trackRestore !== restore) return;
313
- const top = trackStack.pop();
314
- if (top?.get !== get) {
315
- if (top) trackStack.push(top);
316
- const i = trackStack.findIndex((frame) => frame.get === get);
317
- if (i !== -1) trackStack.splice(i, 1);
318
- }
319
- trackGet = trackStack.at(-1)?.get;
320
- fiber.trackRestore = void 0;
321
- };
322
- trackStack.push({
323
- deps,
324
- fiber,
325
- get
326
- });
327
- trackGet = get;
328
- fiber.trackRestore = restore;
329
- try {
330
- const result = withFiber(fiber, () => fn());
331
- if (result instanceof Promise) {
332
- const settle = async () => {
333
- try {
334
- const value = await result;
335
- restore();
336
- if (fiber.closed) return;
337
- onOk({
338
- deps,
339
- value
340
- });
341
- } catch (error) {
342
- restore();
343
- if (fiber.closed) return;
344
- onErr(error);
345
- }
346
- };
347
- settle();
348
- return;
349
- }
350
- restore();
351
- onOk({
352
- deps,
353
- value: result
354
- });
355
- } catch (error) {
356
- restore();
357
- onErr(error);
358
- }
359
- };
360
- const subscribeRenderDeps = (fiber, deps, run) => {
361
- let scheduled = false;
362
- const schedule = () => {
363
- if (scheduled) return;
364
- scheduled = true;
365
- const seen = fiber.renderGen;
366
- fiber.runtime.later(() => {
367
- scheduled = false;
368
- if (fiber.renderGen !== seen) return;
369
- run();
370
- });
371
- };
372
- const unsubs = [];
373
- for (const dep of deps) unsubs.push(fiber.registry.subscribe(dep, schedule));
374
- return () => {
375
- for (const unsub of unsubs) unsub();
376
- };
377
- };
378
- const wrapHandle = (atom, fiber) => {
379
- const { registry } = fiber;
380
- const read = (() => {
381
- guardHandleFiber(read);
382
- return readTracked(atom, registry);
383
- });
384
- handleOwner.set(read, fiber);
385
- Object.defineProperties(read, {
386
- $$atom: { value: 1 },
387
- atom: { value: atom },
388
- set: { value: (n) => {
389
- guardHandleFiber(read);
390
- if (Atom.isWritable(atom)) registry.set(atom, n);
391
- } },
392
- update: { value: (f) => {
393
- guardHandleFiber(read);
394
- if (Atom.isWritable(atom)) registry.update(atom, f);
395
- } }
396
- });
397
- return read;
398
- };
399
- const makeAtom = (init, options) => {
400
- const fiber = getFiber();
401
- const snap = fiber.runtime.hydrateValues;
402
- const { atom: a, fresh } = usePrimitiveSlot(fiber, () => {
403
- let seed = init;
404
- if (snap && !Atom.isAtom(init) && !Effect$1.isEffect(init) && !Stream$1.isStream(init)) {
405
- const v = snap[fiber.runtime.hydrateI];
406
- fiber.runtime.hydrateI += 1;
407
- if (v !== void 0) seed = v;
408
- }
409
- if (Atom.isAtom(seed)) return seed;
410
- if (Effect$1.isEffect(seed) || Stream$1.isStream(seed)) return Atom.make(seed);
411
- const made = Atom.make(seed);
412
- const eq = options?.equals;
413
- if (eq === void 0) return holdState(fiber, made);
414
- const equalsFn = eq === "structural" ? structuralEqual : eq;
415
- return holdState(fiber, Atom.withEquality(made, equalsFn));
416
- });
417
- const handle = wrapHandle(a, fiber);
418
- if (fresh && fiber.runtime.ssr) fiber.runtime.ssrValues.push(fiber.registry.get(a));
419
- return handle;
420
- };
421
- const atomLazy = (init) => {
422
- const fiber = getFiber();
423
- const snap = fiber.runtime.hydrateValues;
424
- const { atom: a, fresh } = usePrimitiveSlot(fiber, () => {
425
- let seed = init();
426
- if (snap) {
427
- const v = snap[fiber.runtime.hydrateI];
428
- fiber.runtime.hydrateI += 1;
429
- if (v !== void 0) seed = v;
430
- }
431
- return holdState(fiber, Atom.make(seed));
432
- });
433
- const handle = wrapHandle(a, fiber);
434
- if (fresh && fiber.runtime.ssr) fiber.runtime.ssrValues.push(fiber.registry.get(a));
435
- return handle;
436
- };
437
- const atom = Object.assign(makeAtom, { lazy: atomLazy });
438
- const { batch } = Atom;
439
-
440
- //#endregion
441
- //#region src/interpret.ts
442
- const isInstruction = (x) => isObject(x) && x.$$ilhaOp === 1;
443
- const interpret = (gen, fiber) => {
444
- const iter = gen();
445
- const resume = (send, thrown) => {
446
- if (fiber.closed) return;
447
- withFiber(fiber, () => {
448
- if (fiber.inFlight) throw new Error("ilha: re-enter while Instruction in flight");
449
- let step;
450
- try {
451
- step = thrown === void 0 ? iter.next(send) : iter.throw(thrown);
452
- } catch (error) {
453
- fiber.fail(asFailure(error));
454
- return;
455
- }
456
- if (step.done) {
457
- if (step.value !== void 0) fiber.paint(step.value);
458
- return;
459
- }
460
- let effect;
461
- if (isInstruction(step.value)) ({effect} = step.value);
462
- else if (Effect$1.isEffect(step.value)) effect = step.value;
463
- if (effect) {
464
- fiber.inFlight = true;
465
- fiber.run(effect, (a) => {
466
- fiber.inFlight = false;
467
- resume(a);
468
- }, (e) => {
469
- fiber.inFlight = false;
470
- resume(void 0, new Error(failureMessage(e)));
471
- });
472
- return;
473
- }
474
- fiber.paint(step.value);
475
- resume();
476
- });
477
- };
478
- resume();
479
- };
480
-
481
- //#endregion
482
- //#region src/start.ts
483
- const isGen = (x) => {
484
- if (x === null || x === void 0) return false;
485
- const g = x;
486
- return isFunction(g.next) && isFunction(g.throw);
487
- };
488
- const finish = (fiber, view) => {
489
- if (fiber.closed) return;
490
- if (view !== void 0) fiber.paint(view);
491
- };
492
- const runSetup = (fiber, fn) => {
493
- const disposeRenderSub = () => {
494
- fiber.renderSub?.();
495
- fiber.renderSub = void 0;
496
- };
497
- const api = {
498
- commit: (out, deps) => {
499
- if (isGen(out)) {
500
- disposeRenderSub();
501
- interpret((() => out), fiber);
502
- return;
503
- }
504
- disposeRenderSub();
505
- if (!fiber.runtime.ssr) fiber.renderSub = subscribeRenderDeps(fiber, deps, () => api.render());
506
- withFiber(fiber, () => {
507
- finish(fiber, out);
508
- });
509
- },
510
- render: () => {
511
- if (fiber.closed) return;
512
- fiber.renderGen = (fiber.renderGen ?? 0) + 1;
513
- fiber.runtime.begin();
514
- beginPrimitiveFrame(fiber);
515
- withTrackGetRun(fiber, () => fn(), ({ value: out, deps }) => {
516
- try {
517
- api.commit(out, deps);
518
- } catch (error) {
519
- withFiber(fiber, () => fiber.fail(asFailure(error)));
520
- } finally {
521
- fiber.runtime.end();
522
- }
523
- }, (e) => {
524
- withFiber(fiber, () => fiber.fail(asFailure(e)));
525
- fiber.runtime.end();
526
- });
527
- }
528
- };
529
- try {
530
- api.render();
531
- } catch (error) {
532
- fiber.fail(asFailure(error));
533
- }
534
- };
535
-
536
- //#endregion
537
- //#region src/events.ts
538
- const ACTION_CALL = Symbol.for("ilha.actionCall");
539
- const eventTypeFromProp = (key) => {
540
- if (key.length < 4) return;
541
- let type;
542
- if (/^on[A-Z]/u.test(key)) type = key.slice(2).toLowerCase();
543
- else if (/^on[a-z]{3,}$/u.test(key)) type = key.slice(2).toLowerCase();
544
- else return;
545
- return type.length >= 3 ? type : void 0;
546
- };
547
- const isEventProp = (key) => eventTypeFromProp(key) !== void 0;
548
- const dispatch = (el, type, ev) => {
549
- const fiber = el.__ilhaFiber;
550
- if (!fiber || fiber.closed) return;
551
- const fn = el.__ilhaHandlers?.get(type);
552
- if (!isFunction(fn)) return;
553
- const out = fn(ev);
554
- if (Effect$1.isEffect(out)) fiber.run(out);
555
- };
556
- /** One native listener per type; it reads the latest handler at dispatch. */
557
- const listen = (el, type) => {
558
- el.__ilhaEvents ??= /* @__PURE__ */ new Set();
559
- if (el.__ilhaEvents.has(type)) return;
560
- el.__ilhaEvents.add(type);
561
- el.addEventListener(type, (ev) => {
562
- dispatch(el, type, ev);
563
- });
564
- };
565
- const bindSsrActions = (el, props, fiber) => {
566
- const seen = /* @__PURE__ */ new Set();
567
- for (const key of Object.keys(props)) {
568
- const type = eventTypeFromProp(key);
569
- if (!type || seen.has(type)) continue;
570
- seen.add(type);
571
- const fn = props[key];
572
- if (!isFunction(fn)) continue;
573
- const branded = fn[ACTION_CALL];
574
- const rec = branded?.k ? {
575
- a: branded.a ?? [],
576
- k: branded.k
577
- } : void 0;
578
- if (!rec) continue;
579
- const id = `${type}:${fiber.runtime.ssrEventI}`;
580
- fiber.runtime.ssrEventI += 1;
581
- fiber.runtime.ssrActions[id] = rec;
582
- const existing = el.dataset.ilhaOn;
583
- el.dataset.ilhaOn = existing ? `${existing},${id}` : id;
584
- }
585
- };
586
- const bindEvents = (node, props, fiber) => {
587
- const el = node;
588
- if (fiber.runtime.ssr) {
589
- bindSsrActions(el, props, fiber);
590
- return;
591
- }
592
- const handlers = /* @__PURE__ */ new Map();
593
- for (const key of Object.keys(props)) {
594
- const type = eventTypeFromProp(key);
595
- if (type && !handlers.has(type)) handlers.set(type, props[key]);
596
- }
597
- el.__ilhaHandlers = handlers;
598
- el.__ilhaFiber = fiber;
599
- for (const type of handlers.keys()) listen(el, type);
600
- };
601
- /**
602
- * The morph kept `live` in place of the freshly rendered `fresh`: hand the
603
- * render's handlers to the live node, so closures never go stale and handlers
604
- * added on a later render still fire.
605
- */
606
- const adoptEvents = (live, fresh) => {
607
- const from = fresh;
608
- if (!from.__ilhaFiber) return;
609
- const to = live;
610
- to.__ilhaHandlers = from.__ilhaHandlers;
611
- to.__ilhaFiber = from.__ilhaFiber;
612
- for (const type of from.__ilhaHandlers?.keys() ?? []) listen(to, type);
613
- };
614
-
615
- //#endregion
616
- //#region src/morph.ts
617
- const PRESERVE_ATTR = "data-morph-preserve";
618
- const morphKeyOf = (el) => {
619
- const k = el.getAttribute(KEY_ATTR);
620
- if (k !== null) return `k:${k}`;
621
- const s = el.getAttribute(SLOT_ATTR);
622
- return s === null ? null : `s:${s}`;
623
- };
624
- /** Whether `data-morph-preserve` on `el` lists `token`. */
625
- const hasPreserveToken = (el, token) => {
626
- const custom = el.getAttribute(PRESERVE_ATTR);
627
- if (custom === null) return false;
628
- for (const listed of custom.split(/\s+/u)) if (listed === token) return true;
629
- return false;
630
- };
631
- const shouldPreserveMorphAttr = (el, name) => {
632
- if (name === "value" || name === "checked" || name === "selected") return true;
633
- if (name === PRESERVE_ATTR) return el.hasAttribute(PRESERVE_ATTR);
634
- return hasPreserveToken(el, name);
635
- };
636
- const syncAttributes = (from, to) => {
637
- for (const { name, value } of to.attributes) {
638
- if (shouldPreserveMorphAttr(from, name)) continue;
639
- if (from.getAttribute(name) !== value) from.setAttribute(name, value);
640
- }
641
- const names = [];
642
- for (const { name } of from.attributes) names.push(name);
643
- for (const name of names) {
644
- if (name === "open" && !to.hasAttribute("open")) continue;
645
- if (shouldPreserveMorphAttr(from, name)) continue;
646
- if (!to.hasAttribute(name)) from.removeAttribute(name);
647
- }
648
- };
649
- /** Deepest focused element, looking through open shadow roots. */
650
- const deepActiveElement = () => {
651
- let active = document.activeElement;
652
- while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
653
- return active;
654
- };
655
- const snapshotRanges = (active) => {
656
- if (!active.isContentEditable) return null;
657
- const sel = document.getSelection();
658
- if (!sel || sel.rangeCount === 0) return null;
659
- const ranges = [];
660
- for (let i = 0; i < sel.rangeCount; i += 1) {
661
- const range = sel.getRangeAt(i);
662
- if (active.contains(range.startContainer)) ranges.push(range.cloneRange());
663
- }
664
- return ranges.length > 0 ? ranges : null;
665
- };
666
- const snapshotFocus = () => {
667
- if (globalThis.document === void 0) return null;
668
- const active = deepActiveElement();
669
- if (!(active instanceof HTMLElement) || active === document.body) return null;
670
- let selection = null;
671
- try {
672
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) selection = {
673
- dir: active.selectionDirection,
674
- end: active.selectionEnd,
675
- start: active.selectionStart
676
- };
677
- } catch {}
678
- return {
679
- active,
680
- ranges: snapshotRanges(active),
681
- selection
682
- };
683
- };
684
- const restoreRanges = (ranges) => {
685
- const sel = document.getSelection();
686
- if (!sel || !ranges.every((r) => r.startContainer.isConnected)) return;
687
- sel.removeAllRanges();
688
- for (const r of ranges) sel.addRange(r);
689
- };
690
- const restoreFocus = (snapshot) => {
691
- if (!snapshot || !snapshot.active.isConnected) return;
692
- try {
693
- if (deepActiveElement() !== snapshot.active) {
694
- snapshot.active.focus({ preventScroll: true });
695
- if (snapshot.ranges) restoreRanges(snapshot.ranges);
696
- }
697
- const sel = snapshot.selection;
698
- if (sel && sel.start !== null) {
699
- const el = snapshot.active;
700
- if (el.selectionStart !== sel.start || el.selectionEnd !== sel.end) el.setSelectionRange(sel.start, sel.end, sel.dir ?? "none");
701
- }
702
- } catch {}
703
- };
704
- const STAND_IN_ATTR = "data-ilha-reuse";
705
- const standIns = /* @__PURE__ */ new WeakMap();
706
- /** Live nodes with an outstanding stand-in: the morph must not consume them. */
707
- const claimed = /* @__PURE__ */ new WeakSet();
708
- /** Placeholder for a connected live node inside a scratch render tree. */
709
- const standInFor = (live) => {
710
- const el = document.createElementNS(live.namespaceURI, live.localName);
711
- for (const name of [SLOT_ATTR, KEY_ATTR]) {
712
- const v = live.getAttribute(name);
713
- if (v !== null) el.setAttribute(name, v);
714
- }
715
- el.setAttribute(STAND_IN_ATTR, "");
716
- standIns.set(el, live);
717
- claimed.add(live);
718
- return el;
719
- };
720
- const liveOf = (node) => {
721
- const live = standIns.get(node);
722
- if (!live) return node;
723
- claimed.delete(live);
724
- return live;
725
- };
726
- /**
727
- * Insert `node` before `ref`. Connected nodes move with `moveBefore` where the
728
- * browser has it, so they keep focus, iframe, dialog, and element state.
729
- */
730
- const place = (parent, node, ref) => {
731
- if (node === ref || node.parentNode === parent && node.nextSibling === ref) return;
732
- if (node.isConnected && parent.isConnected && parent.moveBefore) try {
733
- parent.moveBefore(node, ref);
734
- return;
735
- } catch {}
736
- if (ref) ref.before(node);
737
- else parent.append(node);
738
- };
739
- /** Swap stand-ins nested in `node` (now in the document) for live nodes. */
740
- const resolveNested = (node) => {
741
- if (!(node instanceof Element) || !node.firstElementChild) return;
742
- const nested = node.querySelectorAll(`[${STAND_IN_ATTR}]`);
743
- for (const s of nested) {
744
- const live = liveOf(s);
745
- if (live !== s && s.parentNode) {
746
- place(s.parentNode, live, s);
747
- s.remove();
748
- }
749
- }
750
- };
751
- /**
752
- * Put a rendered node into the live tree: stand-ins become their live nodes,
753
- * and stand-ins nested in fresh subtrees resolve once the subtree is attached.
754
- */
755
- const insertRendered = (parent, node, ref) => {
756
- const live = liveOf(node);
757
- place(parent, live, ref);
758
- if (live === node) resolveNested(node);
759
- };
760
- /** Longest increasing run of old indexes: those nodes never move. */
761
- const stableSet = (nodes, oldIndex) => {
762
- const tails = [];
763
- const prev = [];
764
- for (let i = 0; i < nodes.length; i += 1) {
765
- const node = nodes[i];
766
- const idx = node ? oldIndex.get(node) : void 0;
767
- if (idx === void 0) {
768
- prev.push(-1);
769
- continue;
770
- }
771
- let lo = 0;
772
- let hi = tails.length;
773
- while (lo < hi) {
774
- const mid = Math.floor((lo + hi) / 2);
775
- const tail = nodes[tails[mid] ?? 0];
776
- if ((tail ? oldIndex.get(tail) ?? 0 : 0) < idx) lo = mid + 1;
777
- else hi = mid;
778
- }
779
- prev.push(lo > 0 ? tails[lo - 1] ?? -1 : -1);
780
- tails[lo] = i;
781
- }
782
- const stable = /* @__PURE__ */ new Set();
783
- let k = tails.length > 0 ? tails.at(-1) ?? -1 : -1;
784
- while (k >= 0) {
785
- const node = nodes[k];
786
- if (node) stable.add(node);
787
- k = prev[k] ?? -1;
788
- }
789
- return stable;
790
- };
791
- /**
792
- * Make `parent`'s children exactly `nodes` with the fewest moves. Nodes that
793
- * keep their relative order stay attached; removed nodes leave first.
794
- */
795
- const placeChildren = (parent, rendered) => {
796
- const focus = snapshotFocus();
797
- const nodes = rendered.map(liveOf);
798
- const wanted = new Set(nodes);
799
- const oldIndex = /* @__PURE__ */ new Map();
800
- let child = parent.firstChild;
801
- while (child) {
802
- const next = child.nextSibling;
803
- if (wanted.has(child)) oldIndex.set(child, oldIndex.size);
804
- else child.remove();
805
- child = next;
806
- }
807
- const stable = stableSet(nodes, oldIndex);
808
- let next = null;
809
- for (let i = nodes.length - 1; i >= 0; i -= 1) {
810
- const node = nodes[i];
811
- if (!node) continue;
812
- if (!stable.has(node)) {
813
- place(parent, node, next);
814
- if (!oldIndex.has(node)) resolveNested(node);
815
- }
816
- next = node;
817
- }
818
- restoreFocus(focus);
819
- };
820
- const buildKeyedIndex = (parent) => {
821
- let fromKeyed = null;
822
- for (const child of parent.children) {
823
- const k = morphKeyOf(child);
824
- if (k !== null) {
825
- fromKeyed ??= /* @__PURE__ */ new Map();
826
- if (!fromKeyed.has(k)) fromKeyed.set(k, child);
827
- }
828
- }
829
- return fromKeyed;
830
- };
831
- const collectKeys = (parent) => {
832
- const keys = /* @__PURE__ */ new Set();
833
- for (const child of parent.children) {
834
- const k = morphKeyOf(child);
835
- if (k !== null) keys.add(k);
836
- }
837
- return keys;
838
- };
839
- const morphInput = (fromEl, toEl) => {
840
- syncAttributes(fromEl, toEl);
841
- const from = fromEl;
842
- const to = toEl;
843
- const toChecked = toEl.hasAttribute("checked");
844
- if (toChecked !== fromEl.hasAttribute("checked")) {
845
- if (toChecked) fromEl.setAttribute("checked", "");
846
- else fromEl.removeAttribute("checked");
847
- }
848
- if (toChecked && from.checked !== to.checked) from.checked = to.checked;
849
- const toValue = toEl.getAttribute("value");
850
- if (toValue !== fromEl.getAttribute("value")) {
851
- if (toValue === null) fromEl.removeAttribute("value");
852
- else fromEl.setAttribute("value", toValue);
853
- }
854
- if (toValue !== null && from.value !== to.value) from.value = to.value;
855
- };
856
- const morphTextarea = (fromEl, toEl) => {
857
- syncAttributes(fromEl, toEl);
858
- const from = fromEl;
859
- const to = toEl;
860
- if (to.textContent !== "" || from.textContent !== to.textContent) {
861
- if (from.value !== to.value) from.value = to.value;
862
- if (from.textContent !== to.textContent) from.textContent = to.textContent;
863
- }
864
- };
865
- const morphSelect = (fromEl, toEl, api) => {
866
- const before = /* @__PURE__ */ new Map();
867
- const fromSelect = fromEl;
868
- for (const o of fromSelect.options) before.set(o, {
869
- attr: o.hasAttribute("selected"),
870
- live: o.selected
871
- });
872
- syncAttributes(fromEl, toEl);
873
- api.morphChildren(fromEl, toEl);
874
- const options = [...fromSelect.options];
875
- if (options.some((o) => o.hasAttribute("selected") !== (before.get(o)?.attr ?? false))) for (const o of options) o.selected = o.hasAttribute("selected");
876
- else for (const o of options) {
877
- const prev = before.get(o);
878
- if (prev && o.selected !== prev.live) o.selected = prev.live;
879
- }
880
- };
881
- /** Rendered element → the live element the morph kept in its place. */
882
- const adoptedBy = /* @__PURE__ */ new WeakMap();
883
- /** The element a rendered element ended up as in the document. */
884
- const committedElement = (el) => adoptedBy.get(el) ?? el;
885
- /** `live` stays in the document for `fresh`: move the render's bindings. */
886
- const adopt = (live, fresh) => {
887
- adoptedBy.set(fresh, live);
888
- adoptEvents(live, fresh);
889
- };
890
- /**
891
- * A contenteditable the render leaves empty is uncontrolled: the user's
892
- * content (and caret) survives, like an uncontrolled textarea.
893
- */
894
- const isUncontrolledEditable = (from, to) => {
895
- const mode = to.getAttribute("contenteditable");
896
- return mode !== null && mode !== "false" && !to.hasChildNodes() && from.getAttribute("contenteditable") === mode;
897
- };
898
- /** Swap `fromNode` for a rendered node without ever detaching live nodes. */
899
- const replaceRendered = (fromNode, toNode) => {
900
- const parent = fromNode.parentNode;
901
- if (!parent) return;
902
- insertRendered(parent, toNode, fromNode);
903
- fromNode.remove();
904
- };
905
- const morphElementPair = (fromEl, toEl, api) => {
906
- if (fromEl.localName !== toEl.localName) {
907
- replaceRendered(fromEl, toEl);
908
- return;
909
- }
910
- const toSlot = toEl.getAttribute(SLOT_ATTR);
911
- const fromSlot = fromEl.getAttribute(SLOT_ATTR);
912
- if (toSlot !== null && fromSlot === toSlot) return;
913
- if (toSlot !== null) {
914
- replaceRendered(fromEl, toEl);
915
- return;
916
- }
917
- adopt(fromEl, toEl);
918
- if (fromEl.localName === "input") {
919
- morphInput(fromEl, toEl);
920
- return;
921
- }
922
- if (fromEl.localName === "select") {
923
- morphSelect(fromEl, toEl, api);
924
- return;
925
- }
926
- const keepChildren = hasPreserveToken(fromEl, "children") || hasPreserveToken(toEl, "children") || isUncontrolledEditable(fromEl, toEl);
927
- syncAttributes(fromEl, toEl);
928
- if (keepChildren) return;
929
- if (fromEl.localName === "textarea") morphTextarea(fromEl, toEl);
930
- else api.morphChildren(fromEl, toEl);
931
- };
932
- /** Keyed node the new render drops and no stand-in still needs. */
933
- const isStaleKeyed = (node, toKeys) => {
934
- if (!(node instanceof Element) || claimed.has(node)) return false;
935
- const key = morphKeyOf(node);
936
- return key !== null && !toKeys.has(key);
937
- };
938
- const alignKeyedNode = (fromParent, fromNode, toNode, fromKeyed, toKeys) => {
939
- const toKey = toNode.nodeType === 1 ? morphKeyOf(toNode) : null;
940
- let current = fromNode;
941
- if (toKey !== null) {
942
- const match = fromKeyed.get(toKey);
943
- if (match) {
944
- fromKeyed.delete(toKey);
945
- while (current && current !== match && isStaleKeyed(current, toKeys)) {
946
- const next = current.nextSibling;
947
- current.remove();
948
- current = next ?? void 0;
949
- }
950
- if (match !== current) {
951
- place(fromParent, match, current ?? null);
952
- current = match;
953
- }
954
- }
955
- }
956
- if (current instanceof Element) {
957
- const fromKey = morphKeyOf(current);
958
- if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
959
- insertRendered(fromParent, toNode, current);
960
- return "continue";
961
- }
962
- }
963
- return current;
964
- };
965
- const morphChild = ({ api, fromParent, fromNode, toNode }) => {
966
- if (standIns.has(toNode)) {
967
- insertRendered(fromParent, toNode, fromNode ?? null);
968
- return;
969
- }
970
- if (!fromNode || claimed.has(fromNode)) {
971
- insertRendered(fromParent, toNode, fromNode ?? null);
972
- return;
973
- }
974
- if (fromNode.nodeType !== toNode.nodeType) {
975
- replaceRendered(fromNode, toNode);
976
- return;
977
- }
978
- if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
979
- if (fromNode.nodeValue !== toNode.nodeValue) fromNode.nodeValue = toNode.nodeValue;
980
- return;
981
- }
982
- if (fromNode.nodeType === 1) morphElementPair(fromNode, toNode, api);
983
- };
984
- const api = { morphChildren: (fromParent, toParent) => {
985
- const toNodes = [...toParent.childNodes];
986
- const fromKeyed = buildKeyedIndex(fromParent);
987
- const toKeys = fromKeyed === null ? null : collectKeys(toParent);
988
- for (let i = 0; i < toNodes.length; i += 1) {
989
- const toNode = toNodes[i];
990
- if (!toNode) continue;
991
- let fromNode = fromParent.childNodes[i];
992
- if (fromKeyed !== null && toKeys !== null) {
993
- const aligned = alignKeyedNode(fromParent, fromNode, toNode, fromKeyed, toKeys);
994
- if (aligned === "continue") continue;
995
- fromNode = aligned;
996
- }
997
- morphChild({
998
- api,
999
- fromNode,
1000
- fromParent,
1001
- toNode
1002
- });
1003
- }
1004
- while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild?.remove();
1005
- } };
1006
- const morphInner = (from, to) => {
1007
- const focus = snapshotFocus();
1008
- api.morphChildren(from, to);
1009
- restoreFocus(focus);
1010
- };
1011
-
1012
- //#endregion
1013
- //#region src/security.ts
1014
- const SAFE_CSS_PROP_RE = /^(?:-{2}[a-zA-Z][a-zA-Z0-9-]*|-?[a-zA-Z][a-zA-Z0-9-]*)$/u;
1015
- const URL_ATTRS = /* @__PURE__ */ new Set([
1016
- "href",
1017
- "src",
1018
- "srcset",
1019
- "imagesrcset",
1020
- "action",
1021
- "formaction",
1022
- "cite",
1023
- "data",
1024
- "poster"
1025
- ]);
1026
- const SAFE_DATA_IMAGE_RE = /^data:image\/(?<fmt>png|jpe?g|gif|webp|avif)[;,]/iu;
1027
- const UNSAFE_SCHEME_RE = /^(?:javascript|vbscript):/iu;
1028
- const DATA_IMAGE_CONTEXTS = /* @__PURE__ */ new Set([
1029
- "img:src",
1030
- "source:srcset",
1031
- "link:imagesrcset"
1032
- ]);
1033
- const isBlockedUrlChar = (code) => code <= 32 || code === 160 || code === 5760 || code >= 8192 && code <= 8205 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
1034
- const normalizeUrl = (value) => {
1035
- let out = "";
1036
- for (const ch of value) {
1037
- const code = ch.codePointAt(0) ?? 0;
1038
- if (!isBlockedUrlChar(code)) out += ch;
1039
- }
1040
- return out;
1041
- };
1042
- const checkUrl = (normalized, allowDataImage) => {
1043
- if (UNSAFE_SCHEME_RE.test(normalized)) return false;
1044
- if (/^data:/iu.test(normalized)) return allowDataImage && SAFE_DATA_IMAGE_RE.test(normalized);
1045
- return true;
1046
- };
1047
- const isUrlAttributeName = (name) => {
1048
- const lower = name.toLowerCase();
1049
- return URL_ATTRS.has(lower) || /:(?<attr>href|src|srcset|imagesrcset|action|formaction|cite|data|poster)$/u.test(lower);
1050
- };
1051
- const isSafeUrlAttrValue = (tagName, attrName, value) => {
1052
- const attr = attrName.toLowerCase();
1053
- const allowDataImage = DATA_IMAGE_CONTEXTS.has(`${tagName.toLowerCase()}:${attr}`) && !attr.startsWith("srcset") && attr !== "imagesrcset";
1054
- if (/^(?:srcset|imagesrcset)$/iu.test(attr)) return value.split(",").every((candidate) => {
1055
- const url = candidate.trim().split(/\s+/u)[0] ?? "";
1056
- return url === "" || checkUrl(normalizeUrl(url), false);
1057
- });
1058
- return checkUrl(normalizeUrl(value), allowDataImage);
1059
- };
1060
- const isSafeStyleValue = (str) => {
1061
- if (/[<>{};]/u.test(str) || /expression\(/iu.test(str) || /javascript:/iu.test(str) || /data:/iu.test(str)) return false;
1062
- if (/\\/u.test(str)) return false;
1063
- const urlRe = /url\s*\(\s*(?<inner>[^)]+)\s*\)/giu;
1064
- let m;
1065
- while ((m = urlRe.exec(str)) !== null) {
1066
- const inner = (m.groups?.inner ?? "").trim().replaceAll(/^['"]|['"]$/gu, "");
1067
- if (!checkUrl(normalizeUrl(inner), false)) return false;
1068
- }
1069
- return true;
1070
- };
1071
- const serializeStyleEntries = (value) => Object.entries(value).map(([k, v]) => {
1072
- if (!SAFE_CSS_PROP_RE.test(k)) return "";
1073
- const prop = k.replaceAll(/[A-Z]/gu, (m) => `-${m.toLowerCase()}`);
1074
- const str = String(v);
1075
- if (!isSafeStyleValue(str)) return "";
1076
- return `${prop}:${str}`;
1077
- }).filter(Boolean).join(";");
1078
- const serializeStyleAttr = (value) => {
1079
- if (isString(value)) {
1080
- const obj = {};
1081
- for (const decl of value.split(";")) {
1082
- const colon = decl.indexOf(":");
1083
- if (colon === -1) continue;
1084
- const prop = decl.slice(0, colon).trim();
1085
- const val = decl.slice(colon + 1).trim();
1086
- if (prop) obj[prop] = val;
1087
- }
1088
- return serializeStyleEntries(obj);
1089
- }
1090
- return serializeStyleEntries(value);
1091
- };
1092
-
1093
- //#endregion
1094
- //#region src/unsafe.ts
1095
- /**
1096
- * Mark a string as pre-rendered HTML and paint it without escaping.
1097
- * Use it for markup you built or sanitized yourself — SVG from an external
1098
- * library, Markdown rendered to HTML, embed codes. Never pass untrusted
1099
- * user input: unlike interpolated strings, this content is not escaped and
1100
- * scripts inserted this way do not run, but event attributes and other
1101
- * markup still apply.
1102
- */
1103
- const unsafe = (html) => ({
1104
- $$ilhaUnsafe: 1,
1105
- html
1106
- });
1107
- const isUnsafeHtml = (x) => {
1108
- if (!isObject(x)) return false;
1109
- return x.$$ilhaUnsafe === 1;
1110
- };
1111
-
1112
- //#endregion
1113
- //#region src/paint-core.ts
1114
- const SVG_NS = "http://www.w3.org/2000/svg";
1115
- const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
1116
- const HTML_NS = "http://www.w3.org/1999/xhtml";
1117
- const disposeHoles = (fiber, opts) => {
1118
- const keep = [];
1119
- for (const h of fiber.holes) {
1120
- if (opts?.morph && h.keepOnMorph) {
1121
- keep.push(h);
1122
- continue;
1123
- }
1124
- h.dispose();
1125
- }
1126
- fiber.holes = keep;
1127
- };
1128
- const parentNsOf = (root) => {
1129
- if ("namespaceURI" in root) return root.namespaceURI ?? null;
1130
- return null;
1131
- };
1132
- const elementNs = (lower, parentNs) => {
1133
- if (lower === "svg") return SVG_NS;
1134
- if (lower === "math") return MATHML_NS;
1135
- if (lower === "foreignobject") return HTML_NS;
1136
- if (parentNs === "http://www.w3.org/2000/svg" || parentNs === "http://www.w3.org/1998/Math/MathML") return parentNs;
1137
- };
1138
- const canMorphRoot = (root) => typeof Element !== "undefined" && typeof document !== "undefined" && root instanceof Element && root.childNodes.length > 0;
1139
- /** Close a child hole through the parent's hole list, so it disposes once. */
1140
- const releaseHole = (fiber, hole) => {
1141
- const idx = fiber.holes.findIndex((h) => h.holeFiber === hole);
1142
- if (idx === -1) closeFiber(hole);
1143
- else {
1144
- const [removed] = fiber.holes.splice(idx, 1);
1145
- removed?.dispose();
1146
- }
1147
- };
1148
- const sweepComponentSlots = (fiber) => {
1149
- const cframe = fiber.componentFrame;
1150
- if (!cframe) return;
1151
- const { slots } = cframe;
1152
- for (let j = cframe.i; j < slots.length; j += 1) {
1153
- const s = slots[j];
1154
- if (s && !s.hole.closed) releaseHole(fiber, s.hole);
1155
- slots[j] = void 0;
1156
- }
1157
- slots.length = cframe.i;
1158
- };
1159
- /** Close the keyed components the paint that just ran did not reach. */
1160
- const sweepKeyedHoles = (fiber) => {
1161
- const { keyedHoles, keyedGen } = fiber;
1162
- if (!keyedHoles) return;
1163
- for (const [k, hole] of keyedHoles) {
1164
- if (!hole.closed && hole.keyedSeen === keyedGen) continue;
1165
- keyedHoles.delete(k);
1166
- if (!hole.closed) releaseHole(fiber, hole);
1167
- }
1168
- };
1169
- /**
1170
- * Run `fn` as one paint of `fiber`: the keyed components it materializes stay
1171
- * alive, and the ones it no longer reaches are closed afterwards.
1172
- */
1173
- const pass = (fiber, fn) => {
1174
- fiber.keyedHoles ??= /* @__PURE__ */ new Map();
1175
- fiber.keyedGen = (fiber.keyedGen ?? 0) + 1;
1176
- fn();
1177
- sweepKeyedHoles(fiber);
1178
- };
1179
- /**
1180
- * Dispose a hole fiber's child holes before it repaints, except the component
1181
- * and island holes the repaint can reuse: positional children, keyed
1182
- * children, and islands. Reuse pushes new props instead of remounting, and the
1183
- * sweeps that end the paint close whatever it did not reach.
1184
- */
1185
- const keepReusableHoles = (fiber, cframe, frame) => {
1186
- const kept = /* @__PURE__ */ new Set();
1187
- for (const s of cframe.slots) if (s && !s.hole.closed) kept.add(s.hole);
1188
- for (const hole of fiber.keyedHoles?.values() ?? []) if (!hole.closed) kept.add(hole);
1189
- const keptIslands = new Set(frame.slots);
1190
- const keep = [];
1191
- for (const h of fiber.holes) {
1192
- if (h.holeFiber && kept.has(h.holeFiber) || h.islandSlot && keptIslands.has(h.islandSlot)) {
1193
- keep.push(h);
1194
- continue;
1195
- }
1196
- h.dispose();
1197
- }
1198
- fiber.holes = keep;
1199
- };
1200
- /** Shallow equality of two prop bags, children compared item by item. */
1201
- const sameProps = (prev, next) => {
1202
- if (!prev) return false;
1203
- const keys = Object.keys(next);
1204
- if (keys.length !== Object.keys(prev).length) return false;
1205
- return keys.every((k) => {
1206
- const a = prev[k];
1207
- const b = next[k];
1208
- if (k === "children" && Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => Object.is(x, b[i]));
1209
- return Object.is(a, b);
1210
- });
1211
- };
1212
- /**
1213
- * Rerun a reused component hole with new props, in its own fiber. The props
1214
- * box is updated in place, so every render closure of the hole reads the
1215
- * latest props, including a self-render queued while an async render ran.
1216
- */
1217
- const rerunHole = (hole, run, props) => {
1218
- const box = hole.propsBox ?? { current: props };
1219
- box.current = props;
1220
- hole.propsBox = box;
1221
- runSetup(hole, () => run(box.current));
1222
- };
1223
- /**
1224
- * Run a new component hole's first render after the paint that opened it. A
1225
- * repaint that reached the hole first already rendered it with newer props.
1226
- */
1227
- const mountHoleLater = (fiber, hole, run) => {
1228
- fiber.runtime.later(() => {
1229
- const box = hole.propsBox;
1230
- if (hole.closed || hole.renderGen !== void 0 || !box) return;
1231
- runSetup(hole, () => run(box.current));
1232
- });
1233
- };
1234
- const warnedKeys = /* @__PURE__ */ new Set();
1235
- const warnDuplicateKey = (key) => {
1236
- if (warnedKeys.has(key)) return;
1237
- warnedKeys.add(key);
1238
- console.warn(`ilha: key "${key}" is used by more than one component in the same parent component; keys must be unique there. The duplicate keeps its state by position instead.`);
1239
- };
1240
- const sweepIslandSlots = (fiber) => {
1241
- const frame = fiber.islandFrame;
1242
- if (!frame) return;
1243
- const active = new Set(frame.slots.slice(0, frame.i));
1244
- const keep = [];
1245
- for (const h of fiber.holes) {
1246
- if (h.islandSlot && !active.has(h.islandSlot)) {
1247
- h.dispose();
1248
- continue;
1249
- }
1250
- keep.push(h);
1251
- }
1252
- fiber.holes = keep;
1253
- frame.slots.length = Math.min(frame.slots.length, frame.i);
1254
- };
1255
- const findReusableAtomHost = (fiber, atom) => {
1256
- for (const h of fiber.holes) if (h.atom === atom && h.host?.isConnected && h.holeFiber && !h.holeFiber.closed) return {
1257
- hole: h.holeFiber,
1258
- host: h.host
1259
- };
1260
- };
1261
- const emptyUnsub = () => {};
1262
- const isTextish = (view) => isString(view) || isNumber(view) || isBigInt(view);
1263
- const isIterableView = (view) => {
1264
- if (Array.isArray(view)) return true;
1265
- if (!view || !isObject(view)) return false;
1266
- return Symbol.iterator in view;
1267
- };
1268
- const styleCss = (v) => {
1269
- if (isString(v) || isObject(v)) return serializeStyleAttr(v);
1270
- return "";
1271
- };
1272
- const writeAttr = (el, k, v) => {
1273
- if (v === false || v === null || v === void 0) {
1274
- el.removeAttribute(k);
1275
- return;
1276
- }
1277
- const str = v === true ? "" : String(v);
1278
- if (isUrlAttributeName(k) && !isSafeUrlAttrValue(el.tagName, k, str)) return;
1279
- el.setAttribute(k, str);
1280
- };
1281
- const isSkippedPropKey = (k) => k === "children" || k === "key" || k === "ref" || k.toLowerCase() === "srcdoc" || isEventProp(k) || /^on[A-Z]/u.test(k);
1282
- /**
1283
- * The painter shared by DOM mount and SSR: props, materialization, holes, and
1284
- * keyed reconciliation. Host differences live in `PaintOps`.
1285
- */
1286
- const createPainter = (ops) => {
1287
- let paintDepth = 0;
1288
- const pendingRefs = [];
1289
- const commit = (fn) => {
1290
- paintDepth += 1;
1291
- try {
1292
- fn();
1293
- } finally {
1294
- paintDepth -= 1;
1295
- if (paintDepth === 0) for (const run of pendingRefs.splice(0)) run();
1296
- }
1297
- };
1298
- const applyFormControl = (el, key, v, fiber) => {
1299
- const setProp = (x) => ops.setFormControl(ops.committed(el), key, x);
1300
- if (isAtomHandle(v)) {
1301
- const memo = el;
1302
- if (memo.__ilhaVal === v) return;
1303
- memo.__ilhaVal = v;
1304
- const unsub = fiber.registry.subscribe(v.atom, setProp, { immediate: true });
1305
- fiber.holes.push({ dispose: () => {
1306
- unsub();
1307
- if (memo.__ilhaVal === v) memo.__ilhaVal = void 0;
1308
- } });
1309
- } else setProp(v);
1310
- };
1311
- const trackHole = (parent, hole, extra, meta) => {
1312
- parent.holes.push({
1313
- atom: meta?.atom,
1314
- dispose() {
1315
- extra?.();
1316
- if (meta?.host) ops.disconnect(meta.host);
1317
- closeFiber(hole);
1318
- },
1319
- holeFiber: hole,
1320
- host: meta?.host ? ops.asElement(meta.host) : void 0,
1321
- keepOnMorph: !!(meta?.atom && meta?.host) || !!meta?.keyed
1322
- });
1323
- };
1324
- const insert = (fiber, nodes) => {
1325
- for (const n of nodes) ops.appendRoot(fiber.root, n);
1326
- };
1327
- const applyProps = (el, props, fiber) => {
1328
- for (const [k, v] of Object.entries(props)) {
1329
- if (isSkippedPropKey(k)) continue;
1330
- if (ops.setProp?.(el, k, v) === true) continue;
1331
- if (k === "className") {
1332
- el.setAttribute("class", String(v ?? ""));
1333
- continue;
1334
- }
1335
- if (k === "htmlFor") {
1336
- el.setAttribute("for", String(v ?? ""));
1337
- continue;
1338
- }
1339
- if (k === "style" && v !== null && v !== void 0 && v !== false) {
1340
- const css = styleCss(v);
1341
- if (css) ops.setStyle(el, css);
1342
- continue;
1343
- }
1344
- if (k === "value" || k === "checked" || k === "selected") {
1345
- applyFormControl(el, k, v, fiber);
1346
- continue;
1347
- }
1348
- if (isFunction(v)) continue;
1349
- writeAttr(el, k, v);
1350
- }
1351
- ops.bindEvents(el, props, fiber);
1352
- const { ref } = props;
1353
- if (isFunction(ref)) {
1354
- const refFn = ref;
1355
- const attach = () => refFn(ops.asElement(ops.committed(el)));
1356
- if (paintDepth > 0) pendingRefs.push(attach);
1357
- else attach();
1358
- fiber.holes.push({ dispose: () => refFn(null) });
1359
- }
1360
- };
1361
- const paintFns = {
1362
- keyedPaintHole: () => {},
1363
- materialize: () => []
1364
- };
1365
- const api = {
1366
- applyProps,
1367
- insert,
1368
- materialize: (view, fiber) => paintFns.materialize(view, fiber),
1369
- openHole: (parent) => {
1370
- const id = parent.runtime.nextHole();
1371
- const host = ops.createSlotHost(String(id));
1372
- const child = makeFiber(parent.runtime, ops.asRoot(host), (f, v) => api.paintHole(f, v), {
1373
- onFail: (e) => {
1374
- const error = toError(e);
1375
- if (reportFiberError(child, error)) return;
1376
- parent.runtime.onError?.(error);
1377
- console.error(error);
1378
- api.paintHole(child, errorView(error));
1379
- },
1380
- parent
1381
- });
1382
- return {
1383
- fiber: child,
1384
- nodes: [host]
1385
- };
1386
- },
1387
- paintHole: (fiber, view) => {
1388
- if (view === KEEP) return;
1389
- commit(() => {
1390
- pass(fiber, () => {
1391
- let frame = fiber.islandFrame;
1392
- if (!frame) {
1393
- frame = {
1394
- i: 0,
1395
- slots: []
1396
- };
1397
- fiber.islandFrame = frame;
1398
- }
1399
- frame.i = 0;
1400
- let cframe = fiber.componentFrame;
1401
- if (!cframe) {
1402
- cframe = {
1403
- i: 0,
1404
- slots: []
1405
- };
1406
- fiber.componentFrame = cframe;
1407
- }
1408
- cframe.i = 0;
1409
- const list = Array.isArray(view) ? view : null;
1410
- if (list && list.length > 0 && list.every((v) => isVNode(v) && isFunction(v.type) && v.key !== null && v.key !== void 0)) {
1411
- paintFns.keyedPaintHole(fiber, list);
1412
- sweepComponentSlots(fiber);
1413
- sweepIslandSlots(fiber);
1414
- return;
1415
- }
1416
- keepReusableHoles(fiber, cframe, frame);
1417
- const fresh = api.materialize(view, fiber);
1418
- if (canMorphRoot(fiber.root)) {
1419
- const tmp = document.createElement("div");
1420
- for (const n of fresh) tmp.append(n);
1421
- morphInner(fiber.root, tmp);
1422
- } else ops.placeChildren(fiber.root, fresh);
1423
- sweepComponentSlots(fiber);
1424
- sweepIslandSlots(fiber);
1425
- });
1426
- });
1427
- }
1428
- };
1429
- const materializeAtom = (view, fiber) => {
1430
- const reused = findReusableAtomHost(fiber, view.atom);
1431
- if (reused) return [ops.reuseNode(reused.host)];
1432
- const { fiber: hole, nodes } = api.openHole(fiber);
1433
- const [host] = nodes;
1434
- if (!host) return nodes;
1435
- let unsub = emptyUnsub;
1436
- fiber.runtime.later(() => {
1437
- if (hole.closed) return;
1438
- let keep;
1439
- unsub = fiber.registry.subscribe(view.atom, (v) => {
1440
- if (hole.closed) return;
1441
- const next = unwrap(v, keep);
1442
- if (next === KEEP) return;
1443
- keep = next;
1444
- api.paintHole(hole, next);
1445
- }, { immediate: true });
1446
- });
1447
- trackHole(fiber, hole, () => unsub(), {
1448
- atom: view.atom,
1449
- host
1450
- });
1451
- return nodes;
1452
- };
1453
- const materializeStream = (view, fiber) => {
1454
- const { fiber: hole, nodes } = api.openHole(fiber);
1455
- const src = fiber.runtime.ssr ? view.pipe(Stream$1.take(1)) : view;
1456
- const effect = Effect$1.gen(function* streamPaint() {
1457
- const result = yield* Effect$1.result(Stream$1.runForEach(src, (v) => Effect$1.sync(() => {
1458
- if (!hole.closed) api.paintHole(hole, v);
1459
- })));
1460
- if (result._tag === "Failure" && !hole.closed) api.paintHole(hole, String(result.failure));
1461
- });
1462
- hole.run(effect, fiber.runtime.ssr ? emptyUnsub : void 0);
1463
- trackHole(fiber, hole);
1464
- return nodes;
1465
- };
1466
- const materializeIsland = (type, props, fiber) => {
1467
- let frame = fiber.islandFrame;
1468
- if (!frame) {
1469
- frame = {
1470
- i: 0,
1471
- slots: []
1472
- };
1473
- fiber.islandFrame = frame;
1474
- }
1475
- const { i } = frame;
1476
- frame.i = i + 1;
1477
- const existing = frame.slots[i];
1478
- if (existing && existing.type === type && existing.host.isConnected && isFunction(existing.updateProps)) {
1479
- existing.updateProps(props);
1480
- return [ops.reuseNode(existing.host)];
1481
- }
1482
- if (existing) {
1483
- const idx = fiber.holes.findIndex((h) => h.islandSlot === existing);
1484
- if (idx === -1) existing.unmount?.();
1485
- else {
1486
- const [removed] = fiber.holes.splice(idx, 1);
1487
- removed?.dispose();
1488
- }
1489
- }
1490
- const tag = isString(type[ISLAND_SLOT_TAG]) ? type[ISLAND_SLOT_TAG] : "div";
1491
- const el = ops.createElement(tag);
1492
- el.dataset.ilha = "";
1493
- const mountFn = type[ISLAND_MOUNT_INTERNAL];
1494
- if (isFunction(mountFn)) {
1495
- const handle = mountFn(ops.asElement(el), props);
1496
- const slot = {
1497
- host: ops.asElement(el),
1498
- type,
1499
- unmount: handle?.unmount,
1500
- updateProps: handle?.updateProps
1501
- };
1502
- frame.slots[i] = slot;
1503
- fiber.holes.push({
1504
- dispose: () => {
1505
- ops.disconnect(el);
1506
- handle?.unmount?.();
1507
- if (frame.slots[i] === slot) frame.slots[i] = void 0;
1508
- },
1509
- islandSlot: slot,
1510
- keepOnMorph: true
1511
- });
1512
- }
1513
- return [el];
1514
- };
1515
- const materializeUnkeyedComponent = (run, type, props, fiber) => {
1516
- let { componentFrame: cframe } = fiber;
1517
- if (!cframe) {
1518
- cframe = {
1519
- i: 0,
1520
- slots: []
1521
- };
1522
- fiber.componentFrame = cframe;
1523
- }
1524
- const at = cframe.i;
1525
- cframe.i = at + 1;
1526
- const existing = cframe.slots[at];
1527
- if (existing) {
1528
- const { hole } = existing;
1529
- if (existing.type === type && !hole.closed) {
1530
- rerunHole(hole, run, props);
1531
- return [ops.reuseNode(hole.root)];
1532
- }
1533
- if (!hole.closed) releaseHole(fiber, hole);
1534
- }
1535
- const { fiber: hole, nodes } = api.openHole(fiber);
1536
- hole.propsBox = { current: props };
1537
- hole.componentType = type;
1538
- cframe.slots[at] = {
1539
- hole,
1540
- type
1541
- };
1542
- mountHoleLater(fiber, hole, run);
1543
- trackHole(fiber, hole, void 0, { keyed: true });
1544
- return nodes;
1545
- };
1546
- const materializeKeyedComponent = ({ run, type, props, key, fiber }) => {
1547
- let reuse = fiber.keyedHoles?.get(key);
1548
- const gen = fiber.keyedGen;
1549
- if (reuse && !reuse.closed && reuse.keyedSeen === gen) {
1550
- warnDuplicateKey(key);
1551
- return materializeUnkeyedComponent(run, type, props, fiber);
1552
- }
1553
- if (reuse && !reuse.closed && reuse.componentType !== type) {
1554
- fiber.keyedHoles?.delete(key);
1555
- releaseHole(fiber, reuse);
1556
- reuse = void 0;
1557
- }
1558
- if (reuse && !reuse.closed) {
1559
- reuse.keyedSeen = gen;
1560
- if (!sameProps(reuse.propsBox?.current, props)) rerunHole(reuse, run, props);
1561
- return [ops.reuseNode(reuse.root)];
1562
- }
1563
- const { fiber: khole, nodes: knodes } = api.openHole(fiber);
1564
- khole.propsBox = { current: props };
1565
- khole.componentType = type;
1566
- khole.keyedSeen = gen;
1567
- fiber.keyedHoles?.set(key, khole);
1568
- mountHoleLater(fiber, khole, run);
1569
- trackHole(fiber, khole, void 0, { keyed: true });
1570
- return knodes;
1571
- };
1572
- const materializeComponent = (view, type, fiber) => {
1573
- const props = {
1574
- ...view.props,
1575
- children: view.children
1576
- };
1577
- const branded = type;
1578
- if (branded[ISLAND] === true) return materializeIsland(branded, props, fiber);
1579
- const run = type;
1580
- if (view.key === null || view.key === void 0) return materializeUnkeyedComponent(run, type, props, fiber);
1581
- return materializeKeyedComponent({
1582
- fiber,
1583
- key: String(view.key),
1584
- props,
1585
- run,
1586
- type
1587
- });
1588
- };
1589
- const materializeElement = (view, fiber) => {
1590
- const tag = view.type;
1591
- const lower = tag.toLowerCase();
1592
- const ns = elementNs(lower, parentNsOf(fiber.root));
1593
- const el = ops.createElement(tag, ns);
1594
- if (view.key !== null && view.key !== void 0) el.setAttribute(KEY_ATTR, String(view.key));
1595
- api.applyProps(el, view.props, fiber);
1596
- const childFiber = {
1597
- ...fiber,
1598
- holes: fiber.holes,
1599
- keyedHoles: fiber.keyedHoles,
1600
- root: ops.asRoot(el)
1601
- };
1602
- for (const c of view.children) for (const n of api.materialize(c, childFiber)) el.append(n);
1603
- return [el];
1604
- };
1605
- const materializeVNode = (view, fiber) => {
1606
- if (view.type === Fragment) return view.children.flatMap((c) => api.materialize(c, fiber));
1607
- if (isFunction(view.type)) return materializeComponent(view, view.type, fiber);
1608
- return materializeElement(view, fiber);
1609
- };
1610
- paintFns.materialize = (view, fiber) => {
1611
- if (skip(view)) return [ops.createText("")];
1612
- if (isUnsafeHtml(view)) return ops.createRaw(view.html, fiber.root);
1613
- if (isTextish(view)) return [ops.createText(String(view))];
1614
- if (isAtomHandle(view)) return materializeAtom(view, fiber);
1615
- if (Stream$1.isStream(view)) return materializeStream(view, fiber);
1616
- if (isSetupFn(view)) {
1617
- const { fiber: hole, nodes } = api.openHole(fiber);
1618
- fiber.runtime.later(() => {
1619
- if (!hole.closed) runSetup(hole, view);
1620
- });
1621
- trackHole(fiber, hole);
1622
- return nodes;
1623
- }
1624
- if (isVNode(view)) return materializeVNode(view, fiber);
1625
- if (isIterableView(view)) return [...view].flatMap((c) => api.materialize(c, fiber));
1626
- return [ops.createText(String(view))];
1627
- };
1628
- paintFns.keyedPaintHole = (fiber, views) => {
1629
- const nodes = [];
1630
- for (const v of views) {
1631
- const reuse = fiber.keyedHoles?.get(String(v.key));
1632
- const reusable = reuse && !reuse.closed && reuse.keyedSeen !== fiber.keyedGen && reuse.componentType === v.type;
1633
- if (reuse && reusable) {
1634
- reuse.keyedSeen = fiber.keyedGen;
1635
- const props = {
1636
- ...v.props,
1637
- children: v.children
1638
- };
1639
- if (!sameProps(reuse.propsBox?.current, props)) rerunHole(reuse, v.type, props);
1640
- nodes.push(ops.asNode(reuse.root));
1641
- continue;
1642
- }
1643
- nodes.push(...api.materialize(v, fiber));
1644
- }
1645
- ops.placeChildren(fiber.root, nodes);
1646
- };
1647
- return {
1648
- applyProps: api.applyProps,
1649
- commit,
1650
- disposeHoles: (fiber, opts) => disposeHoles(fiber, opts),
1651
- insert: api.insert,
1652
- materialize: api.materialize,
1653
- paintHole: api.paintHole,
1654
- pass
1655
- };
1656
- };
1657
-
1658
- //#endregion
1659
- export { makeFiber as C, getFiber as S, makeRuntime as T, structuralEqual as _, morphInner as a, clearFiberView as b, bindEvents as c, interpret as d, atom as f, isAtomHandle as g, instr as h, insertRendered as i, eventTypeFromProp as l, handleOwner as m, unsafe as n, placeChildren as o, batch as p, committedElement as r, standInFor as s, createPainter as t, runSetup as u, untrack as v, makeRootFiber as w, closeFiber as x, wrapHandle as y };