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,4 +1,4 @@
1
- import { l as Fragment } from "./types-DsdtZMhG.js";
2
- import { n as JSX } from "./vnode-CpSQZnLM.js";
1
+ import { s as Fragment } from "./types-NhEiBvwy.js";
2
+ import { n as JSX } from "./vnode-DskooLPD.js";
3
3
  import { jsx, jsxDEV, jsxs } from "./jsx-runtime.js";
4
4
  export { Fragment, type JSX, jsx, jsxDEV, jsxs };
@@ -1,4 +1,4 @@
1
- import { i as Fragment } from "./vnode-BHUY61Rm.js";
1
+ import { r as Fragment } from "./vnode-DJ25Jn3D.js";
2
2
  import { jsx, jsxDEV, jsxs } from "./jsx-runtime.js";
3
3
 
4
4
  export { Fragment, jsx, jsxDEV, jsxs };
@@ -1,5 +1,5 @@
1
- import { a as ComponentFn, b as View, f as JsxComponent, l as Fragment, p as PropBag } from "./types-DsdtZMhG.js";
2
- import { n as JSX, t as h } from "./vnode-CpSQZnLM.js";
1
+ import { _ as View, d as JsxComponent, o as ComponentFn, p as PropBag, s as Fragment } from "./types-NhEiBvwy.js";
2
+ import { n as JSX, t as h } from "./vnode-DskooLPD.js";
3
3
  //#region src/jsx-runtime.d.ts
4
4
  export declare const jsx: (type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
5
5
  export declare const jsxs: typeof jsx;
@@ -1,4 +1,4 @@
1
- import { i as Fragment, t as h } from "./vnode-BHUY61Rm.js";
1
+ import { r as Fragment, t as h } from "./vnode-DJ25Jn3D.js";
2
2
 
3
3
  //#region src/jsx-runtime.ts
4
4
  const jsx = (type, props, key) => h(type, key === void 0 ? props : {
@@ -0,0 +1,14 @@
1
+ import { a as Component, f as MountOptions } from "./types-NhEiBvwy.js";
2
+ //#region src/mount.d.ts
3
+ interface RenderToStringOptions {
4
+ /** Embed atom state for hydration. Default `true`. */
5
+ snapshot?: boolean;
6
+ /** Wrap the output in a `<div data-ilha>` host. Default `true`. */
7
+ markers?: boolean;
8
+ /** Serialize after this many ms even if work is still pending. */
9
+ timeout?: number;
10
+ }
11
+ declare const mount: (el: Element, fn: Component, opts?: MountOptions) => (() => void);
12
+ declare const renderToString: (fn: Component, opts?: RenderToStringOptions) => Promise<string>;
13
+ //#endregion
14
+ export { mount as n, renderToString as r, RenderToStringOptions as t };
@@ -0,0 +1,585 @@
1
+ import { a as startRoot, v as ilhaError, y as ilhaWarn } from "./start-B-e1dG5U.js";
2
+ import { c as isObject, n as KEY_ATTR, r as SLOT_ATTR } from "./shared-vpgYXytd.js";
3
+ import { i as bindEvents, r as adoptEvents, t as createPainter } from "./paint-core-D1n4lbpF.js";
4
+
5
+ //#region src/morph.ts
6
+ const PRESERVE_ATTR = "data-morph-preserve";
7
+ const STAND_IN_ATTR = "data-ilha-reuse";
8
+ const FORM_ATTR = /^(?:value|checked|selected)$/u;
9
+ const morphKeyOf = (el) => {
10
+ const k = el.getAttribute(KEY_ATTR);
11
+ if (k !== null) return `k:${k}`;
12
+ const s = el.getAttribute(SLOT_ATTR);
13
+ return s === null ? null : `s:${s}`;
14
+ };
15
+ /** Whether `data-morph-preserve` on `el` lists `token`. */
16
+ const preserves = (el, token) => el.getAttribute(PRESERVE_ATTR)?.split(/\s+/u).includes(token) ?? false;
17
+ /** Attributes the morph leaves to live state or to code outside the render. */
18
+ const keepAttr = (el, name) => FORM_ATTR.test(name) || (name === PRESERVE_ATTR ? el.hasAttribute(name) : preserves(el, name));
19
+ const syncAttributes = (from, to) => {
20
+ for (const { name, value } of to.attributes) if (!keepAttr(from, name) && from.getAttribute(name) !== value) from.setAttribute(name, value);
21
+ for (const name of from.getAttributeNames()) if (!to.hasAttribute(name) && name !== "open" && !keepAttr(from, name)) from.removeAttribute(name);
22
+ };
23
+ /** Deepest focused element, looking through open shadow roots. */
24
+ const deepActiveElement = () => {
25
+ let active = document.activeElement;
26
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
27
+ return active;
28
+ };
29
+ const snapshotFocus = () => {
30
+ if (globalThis.document === void 0) return;
31
+ const active = deepActiveElement();
32
+ if (!(active instanceof HTMLElement) || active === document.body) return;
33
+ const snap = { active };
34
+ try {
35
+ if ((active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) && active.selectionStart !== null) snap.selection = {
36
+ dir: active.selectionDirection,
37
+ end: active.selectionEnd,
38
+ start: active.selectionStart
39
+ };
40
+ } catch {}
41
+ const sel = active.isContentEditable ? document.getSelection() : null;
42
+ if (sel) {
43
+ const ranges = [];
44
+ for (let i = 0; i < sel.rangeCount; i += 1) {
45
+ const range = sel.getRangeAt(i);
46
+ if (active.contains(range.startContainer)) ranges.push(range.cloneRange());
47
+ }
48
+ snap.ranges = ranges.length > 0 ? ranges : void 0;
49
+ }
50
+ return snap;
51
+ };
52
+ const restoreFocus = (snap) => {
53
+ if (!snap?.active.isConnected) return;
54
+ const { active, ranges, selection } = snap;
55
+ try {
56
+ if (deepActiveElement() !== active) {
57
+ active.focus({ preventScroll: true });
58
+ const sel = document.getSelection();
59
+ if (ranges && sel && ranges.every((r) => r.startContainer.isConnected)) {
60
+ sel.removeAllRanges();
61
+ for (const r of ranges) sel.addRange(r);
62
+ }
63
+ }
64
+ if (selection) {
65
+ const el = active;
66
+ if (el.selectionStart !== selection.start || el.selectionEnd !== selection.end) {
67
+ const dir = selection.dir ?? "none";
68
+ el.setSelectionRange(selection.start, selection.end, dir);
69
+ }
70
+ }
71
+ } catch {}
72
+ };
73
+ const standIns = /* @__PURE__ */ new WeakMap();
74
+ /** Live nodes with an outstanding stand-in: the morph must not consume them. */
75
+ const claimed = /* @__PURE__ */ new WeakSet();
76
+ /** Placeholder for a connected live node inside a scratch render tree. */
77
+ const standInFor = (live) => {
78
+ const el = document.createElementNS(live.namespaceURI, live.localName);
79
+ for (const name of [SLOT_ATTR, KEY_ATTR]) {
80
+ const v = live.getAttribute(name);
81
+ if (v !== null) el.setAttribute(name, v);
82
+ }
83
+ el.setAttribute(STAND_IN_ATTR, "");
84
+ standIns.set(el, live);
85
+ claimed.add(live);
86
+ return el;
87
+ };
88
+ const liveOf = (node) => {
89
+ const live = standIns.get(node);
90
+ if (!live) return node;
91
+ claimed.delete(live);
92
+ return live;
93
+ };
94
+ /**
95
+ * Insert `node` before `ref`. Connected nodes move with `moveBefore` where the
96
+ * browser has it, so they keep focus, iframe, dialog, and element state.
97
+ */
98
+ const place = (parent, node, ref) => {
99
+ if (node === ref || node.parentNode === parent && node.nextSibling === ref) return;
100
+ const movable = parent;
101
+ if (node.isConnected && parent.isConnected && movable.moveBefore) try {
102
+ movable.moveBefore(node, ref);
103
+ return;
104
+ } catch {}
105
+ parent.insertBefore(node, ref);
106
+ };
107
+ /** Swap stand-ins nested in `node` (now in the document) for live nodes. */
108
+ const resolveNested = (node) => {
109
+ if (!(node instanceof Element) || !node.firstElementChild) return;
110
+ for (const s of node.querySelectorAll(`[${STAND_IN_ATTR}]`)) {
111
+ const live = liveOf(s);
112
+ if (live !== s && s.parentNode) {
113
+ place(s.parentNode, live, s);
114
+ s.remove();
115
+ }
116
+ }
117
+ };
118
+ /**
119
+ * Put a rendered node into the live tree: stand-ins become their live nodes,
120
+ * and stand-ins nested in fresh subtrees resolve once the subtree is attached.
121
+ */
122
+ const insertRendered = (parent, node, ref) => {
123
+ const live = liveOf(node);
124
+ place(parent, live, ref);
125
+ if (live === node) resolveNested(node);
126
+ };
127
+ /** Positions of the longest run of increasing old indexes: those never move. */
128
+ const stableSet = (olds) => {
129
+ const tails = [];
130
+ const prev = [];
131
+ for (const [i, idx] of olds.entries()) {
132
+ if (idx === void 0) continue;
133
+ let lo = 0;
134
+ let hi = tails.length;
135
+ while (lo < hi) {
136
+ const mid = Math.floor((lo + hi) / 2);
137
+ if ((olds[tails[mid] ?? 0] ?? 0) < idx) lo = mid + 1;
138
+ else hi = mid;
139
+ }
140
+ prev[i] = tails[lo - 1] ?? -1;
141
+ tails[lo] = i;
142
+ }
143
+ const stable = /* @__PURE__ */ new Set();
144
+ for (let k = tails.at(-1) ?? -1; k >= 0; k = prev[k] ?? -1) stable.add(k);
145
+ return stable;
146
+ };
147
+ /**
148
+ * Make `parent`'s children exactly `nodes` with the fewest moves. Nodes that
149
+ * keep their relative order stay attached; removed nodes leave first.
150
+ */
151
+ const placeChildren = (parent, rendered) => {
152
+ const focus = snapshotFocus();
153
+ const nodes = rendered.map(liveOf);
154
+ const wanted = new Set(nodes);
155
+ const oldIndex = /* @__PURE__ */ new Map();
156
+ for (let child = parent.firstChild; child;) {
157
+ const after = child.nextSibling;
158
+ if (wanted.has(child)) oldIndex.set(child, oldIndex.size);
159
+ else child.remove();
160
+ child = after;
161
+ }
162
+ const stable = stableSet(nodes.map((n) => oldIndex.get(n)));
163
+ let next = null;
164
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
165
+ const node = nodes[i] ?? null;
166
+ if (node && !stable.has(i)) {
167
+ place(parent, node, next);
168
+ if (!oldIndex.has(node)) resolveNested(node);
169
+ }
170
+ next = node;
171
+ }
172
+ restoreFocus(focus);
173
+ };
174
+ /** Late-bound: element morphs recurse into children morphed below. */
175
+ const tree = {};
176
+ const controlledBy = {
177
+ checked: /* @__PURE__ */ new WeakSet(),
178
+ selected: /* @__PURE__ */ new WeakSet(),
179
+ value: /* @__PURE__ */ new WeakSet()
180
+ };
181
+ /** Mark a rendered element controlled for `key`: the morph syncs it either way. */
182
+ const markControlled = (el, key) => {
183
+ controlledBy[key].add(el);
184
+ };
185
+ const morphInput = (from, to) => {
186
+ const checked = to.hasAttribute("checked");
187
+ from.toggleAttribute("checked", checked);
188
+ if ((checked || controlledBy.checked.has(to)) && from.checked !== to.checked) from.checked = to.checked;
189
+ const value = to.getAttribute("value");
190
+ if (value === null) from.removeAttribute("value");
191
+ else {
192
+ if (from.getAttribute("value") !== value) from.setAttribute("value", value);
193
+ if (from.value !== to.value) from.value = to.value;
194
+ }
195
+ };
196
+ const morphTextarea = (from, to) => {
197
+ if (controlledBy.value.has(to) || to.textContent !== "" || from.textContent !== to.textContent) {
198
+ if (from.value !== to.value) from.value = to.value;
199
+ if (from.textContent !== to.textContent) from.textContent = to.textContent;
200
+ }
201
+ };
202
+ const morphSelect = (from, to) => {
203
+ const controlled = controlledBy.value.has(to) || [...to.options].some((o) => controlledBy.selected.has(o));
204
+ const before = new Map([...from.options].map((o) => [o, [o.hasAttribute("selected"), o.selected]]));
205
+ tree.morphChildren(from, to);
206
+ const options = [...from.options];
207
+ const attrsChanged = options.some((o) => o.hasAttribute("selected") !== (before.get(o)?.[0] ?? false));
208
+ for (const [i, o] of options.entries()) {
209
+ let want = before.get(o)?.[1] ?? o.selected;
210
+ if (controlled) want = to.options[i]?.selected ?? false;
211
+ else if (attrsChanged) want = o.hasAttribute("selected");
212
+ if (o.selected !== want) o.selected = want;
213
+ }
214
+ };
215
+ /** Rendered element → the live element the morph kept in its place. */
216
+ const adoptedBy = /* @__PURE__ */ new WeakMap();
217
+ /** The element a rendered element ended up as in the document. */
218
+ const committedElement = (el) => adoptedBy.get(el) ?? el;
219
+ /** Swap `fromNode` for a rendered node without ever detaching live nodes. */
220
+ const replaceRendered = (fromNode, toNode) => {
221
+ if (fromNode.parentNode) {
222
+ insertRendered(fromNode.parentNode, toNode, fromNode);
223
+ fromNode.remove();
224
+ }
225
+ };
226
+ /**
227
+ * While hydrating, server slot hosts are replaced by the freshly painted ones:
228
+ * the new hole fibers are rooted in those, so their content must be installed.
229
+ */
230
+ let hydrating = false;
231
+ const morphElementPair = (from, to) => {
232
+ const toSlot = to.getAttribute(SLOT_ATTR);
233
+ if (from.localName !== to.localName || toSlot !== null && hydrating) {
234
+ replaceRendered(from, to);
235
+ return;
236
+ }
237
+ if (toSlot !== null) {
238
+ if (from.getAttribute("data-ilha-slot") !== toSlot) replaceRendered(from, to);
239
+ return;
240
+ }
241
+ adoptedBy.set(to, from);
242
+ adoptEvents(from, to);
243
+ const mode = to.getAttribute("contenteditable");
244
+ const keepChildren = preserves(from, "children") || preserves(to, "children") || mode !== null && mode !== "false" && !to.hasChildNodes() && from.getAttribute("contenteditable") === mode;
245
+ syncAttributes(from, to);
246
+ if (from instanceof HTMLInputElement) morphInput(from, to);
247
+ else if (from instanceof HTMLSelectElement) morphSelect(from, to);
248
+ else if (keepChildren) {} else if (from instanceof HTMLTextAreaElement) morphTextarea(from, to);
249
+ else tree.morphChildren(from, to);
250
+ };
251
+ const morphChild = (parent, from, to) => {
252
+ if (standIns.has(to) || !from || claimed.has(from)) insertRendered(parent, to, from ?? null);
253
+ else if (from.nodeType !== to.nodeType) replaceRendered(from, to);
254
+ else if (from instanceof Element) morphElementPair(from, to);
255
+ else if (from.nodeValue !== to.nodeValue) from.nodeValue = to.nodeValue;
256
+ };
257
+ const keyedIndex = (fromParent, toParent) => {
258
+ const from = /* @__PURE__ */ new Map();
259
+ for (const child of fromParent.children) {
260
+ const k = morphKeyOf(child);
261
+ if (k !== null && !from.has(k)) from.set(k, child);
262
+ }
263
+ if (from.size === 0) return;
264
+ const to = new Set(Array.from(toParent.children, morphKeyOf));
265
+ to.delete(null);
266
+ return {
267
+ from,
268
+ to
269
+ };
270
+ };
271
+ /** A keyed live node the new render drops and no stand-in still needs. */
272
+ const isStale = (node, index) => {
273
+ const key = node instanceof Element && !claimed.has(node) ? morphKeyOf(node) : null;
274
+ return key !== null && !index.to.has(key);
275
+ };
276
+ /**
277
+ * Bring the live node keyed like `to` into position `from`. Returns the node
278
+ * to morph against, or `null` once `to` was inserted ahead of a keyed live
279
+ * node that belongs further down.
280
+ */
281
+ const alignKeyed = (parent, start, to, index) => {
282
+ let from = start;
283
+ const toKey = to instanceof Element ? morphKeyOf(to) : null;
284
+ const match = toKey === null ? void 0 : index.from.get(toKey);
285
+ if (toKey !== null && match) {
286
+ index.from.delete(toKey);
287
+ while (from && from !== match && isStale(from, index)) {
288
+ const next = from.nextSibling;
289
+ from.remove();
290
+ from = next ?? void 0;
291
+ }
292
+ if (match !== from) {
293
+ place(parent, match, from ?? null);
294
+ from = match;
295
+ }
296
+ }
297
+ const fromKey = from instanceof Element ? morphKeyOf(from) : null;
298
+ if (from && fromKey !== null && fromKey !== toKey && index.to.has(fromKey)) {
299
+ insertRendered(parent, to, from);
300
+ return null;
301
+ }
302
+ return from;
303
+ };
304
+ tree.morphChildren = (fromParent, toParent) => {
305
+ const toNodes = [...toParent.childNodes];
306
+ const index = keyedIndex(fromParent, toParent);
307
+ for (const [i, to] of toNodes.entries()) {
308
+ const at = fromParent.childNodes[i];
309
+ const from = index ? alignKeyed(fromParent, at, to, index) : at;
310
+ if (from !== null) morphChild(fromParent, from, to);
311
+ }
312
+ while (fromParent.childNodes.length > toNodes.length) fromParent.lastChild?.remove();
313
+ };
314
+ /** Morph `from`'s children into `to`'s, keeping focus and selection. */
315
+ const morphInner = (from, to) => {
316
+ const focus = snapshotFocus();
317
+ tree.morphChildren(from, to);
318
+ restoreFocus(focus);
319
+ };
320
+ /** Morph the first client render into server markup, replacing slot hosts. */
321
+ const hydrateInner = (from, to) => {
322
+ hydrating = true;
323
+ try {
324
+ morphInner(from, to);
325
+ } finally {
326
+ hydrating = false;
327
+ }
328
+ };
329
+
330
+ //#endregion
331
+ //#region src/paint.ts
332
+ const toggleAttr = (el, name, on) => {
333
+ if (on) el.setAttribute(name, "");
334
+ else el.removeAttribute(name);
335
+ };
336
+ /** The DOM painter: fresh renders morph into the live tree. */
337
+ const { paint: paint$1 } = createPainter({
338
+ bindEvents,
339
+ committed: committedElement,
340
+ createElement: (tag, ns) => ns ? document.createElementNS(ns, tag) : document.createElement(tag),
341
+ createRaw: (html, parent) => {
342
+ const range = document.createRange();
343
+ range.selectNodeContents(parent);
344
+ return [...range.createContextualFragment(html).childNodes];
345
+ },
346
+ createText: (text) => document.createTextNode(text),
347
+ morph: (root, nodes, hydrate) => {
348
+ const el = root;
349
+ if (!el.hasChildNodes()) {
350
+ placeChildren(el, nodes);
351
+ return;
352
+ }
353
+ const tmp = document.createElement("div");
354
+ tmp.append(...nodes);
355
+ if (hydrate) {
356
+ const want = tmp.firstElementChild?.tagName;
357
+ if (want && el.firstElementChild?.tagName !== want) {
358
+ const found = el.firstElementChild?.tagName.toLowerCase();
359
+ ilhaWarn("ILHA006", `expected <${want.toLowerCase()}>, found <${found}>`);
360
+ }
361
+ }
362
+ (hydrate ? hydrateInner : morphInner)(el, tmp);
363
+ },
364
+ placeChildren: (root, nodes) => placeChildren(root, nodes),
365
+ reuseNode: (host) => host.isConnected ? standInFor(host) : host,
366
+ setFormControl: (el, key, v) => {
367
+ const node = el;
368
+ const set = v !== null && v !== void 0;
369
+ if (set) markControlled(el, key);
370
+ if (key !== "value") {
371
+ node[key] = Boolean(v);
372
+ toggleAttr(el, key, Boolean(v));
373
+ return;
374
+ }
375
+ const text = set ? String(v) : "";
376
+ if (el.localName === "textarea") node.defaultValue = text;
377
+ else if (el.localName !== "select") {
378
+ if (set) el.setAttribute("value", text);
379
+ else el.removeAttribute("value");
380
+ }
381
+ node.value = text;
382
+ },
383
+ setStyle: (el, css) => {
384
+ el.style.cssText = css;
385
+ }
386
+ });
387
+
388
+ //#endregion
389
+ //#region src/snapshot.ts
390
+ const MAX_SNAPSHOT_CHARS = 262144;
391
+ const MAX_SNAPSHOT_DEPTH = 32;
392
+ const UNSAFE_KEY = /^(?:__proto__|constructor|prototype)$/u;
393
+ /** Container nesting depth of a parsed JSON value. */
394
+ const depthOf = (value) => {
395
+ if (!Array.isArray(value) && !isObject(value)) return 0;
396
+ let max = 0;
397
+ for (const child of Object.values(value)) max = Math.max(max, depthOf(child));
398
+ return max + 1;
399
+ };
400
+ const encodeSnapshot = (values) => JSON.stringify({ v: values });
401
+ /** Parse a snapshot; oversized, too deep, or malformed input yields `undefined`. */
402
+ const decodeSnapshot = (raw) => {
403
+ if (raw.length > MAX_SNAPSHOT_CHARS) return;
404
+ let parsed;
405
+ try {
406
+ parsed = JSON.parse(raw, (k, v) => UNSAFE_KEY.test(k) ? void 0 : v);
407
+ } catch {
408
+ return;
409
+ }
410
+ if (!isObject(parsed) || depthOf(parsed) > MAX_SNAPSHOT_DEPTH) return;
411
+ const values = "v" in parsed ? parsed.v : void 0;
412
+ return Array.isArray(values) ? values : void 0;
413
+ };
414
+
415
+ //#endregion
416
+ //#region src/ssr-dom.ts
417
+ const VOID = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/u;
418
+ /** Tags whose children are raw text — content is not HTML-escaped. */
419
+ const RAW_TEXT = /^(?:script|style)$/u;
420
+ const escapeText = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
421
+ const escapeAttr = (s) => escapeText(s).replaceAll("\"", "&quot;");
422
+ /** Minimal element for string rendering; the root is an element without a tag. */
423
+ var SsrEl = class {
424
+ kind = "element";
425
+ localName;
426
+ tagName;
427
+ attrs = /* @__PURE__ */ new Map();
428
+ children = [];
429
+ constructor(tag) {
430
+ this.localName = tag.toLowerCase();
431
+ this.tagName = this.localName.toUpperCase();
432
+ }
433
+ setAttribute(name, value) {
434
+ this.attrs.set(name, value);
435
+ }
436
+ getAttribute(name) {
437
+ return this.attrs.get(name) ?? null;
438
+ }
439
+ removeAttribute(name) {
440
+ this.attrs.delete(name);
441
+ }
442
+ append(...nodes) {
443
+ this.children.push(...nodes);
444
+ }
445
+ get textContent() {
446
+ return this.children.map((n) => {
447
+ if (n.kind === "text") return n.text;
448
+ return n.kind === "raw" ? n.html : n.textContent;
449
+ }).join("");
450
+ }
451
+ set textContent(text) {
452
+ this.children = [{
453
+ kind: "text",
454
+ text
455
+ }];
456
+ }
457
+ };
458
+ /**
459
+ * Serialize a node. With `bare`, hole hosts (`data-ilha-slot`) vanish and only
460
+ * their content remains: a render that never hydrates needs no hole markers.
461
+ */
462
+ const serializeNode = (node, bare = false) => {
463
+ if (node.kind === "text") return escapeText(node.text);
464
+ if (node.kind === "raw") return node.html;
465
+ const inner = node.children.map((c) => serializeNode(c, bare)).join("");
466
+ const tag = node.localName;
467
+ if (!tag || bare && node.attrs.has("data-ilha-slot")) return inner;
468
+ let open = `<${tag}`;
469
+ for (const [name, value] of node.attrs) open += ` ${name}="${escapeAttr(value)}"`;
470
+ open += ">";
471
+ if (VOID.test(tag)) return open;
472
+ if (RAW_TEXT.test(tag)) {
473
+ const raw = node.textContent;
474
+ if (new RegExp(`</${tag}\\b`, "iu").test(raw)) throw ilhaError("ILHA007", `<${tag}> contains "</${tag}"`);
475
+ return `${open}${raw}</${tag}>`;
476
+ }
477
+ return `${open}${inner}</${tag}>`;
478
+ };
479
+
480
+ //#endregion
481
+ //#region src/ssr-paint.ts
482
+ const WHITESPACE_RUN = /\s+/gu;
483
+ /**
484
+ * Mark the first option whose value is `value` selected and clear the rest,
485
+ * as setting a live select's `.value` does. Returns whether one matched.
486
+ */
487
+ const selectOption = (parent, value, found) => {
488
+ let matched = found;
489
+ for (const child of parent.children) {
490
+ if (child.kind !== "element") continue;
491
+ if (child.localName === "optgroup") matched = selectOption(child, value, matched);
492
+ else if (child.localName === "option") {
493
+ const optionValue = child.getAttribute("value") ?? child.textContent.trim().replaceAll(WHITESPACE_RUN, " ");
494
+ if (!matched && optionValue === value) {
495
+ child.setAttribute("selected", "");
496
+ matched = true;
497
+ } else child.removeAttribute("selected");
498
+ }
499
+ }
500
+ return matched;
501
+ };
502
+ /** The string painter: every paint rebuilds the host's children. */
503
+ const { paint } = createPainter({
504
+ createElement: (tag) => new SsrEl(tag),
505
+ createRaw: (html) => [{
506
+ html,
507
+ kind: "raw"
508
+ }],
509
+ createText: (text) => ({
510
+ kind: "text",
511
+ text
512
+ }),
513
+ placeChildren: (root, nodes) => {
514
+ const host = root;
515
+ host.children = nodes;
516
+ },
517
+ setFormControl: (el, key, v) => {
518
+ if (key !== "value") {
519
+ if (v) el.setAttribute(key, "");
520
+ else el.removeAttribute(key);
521
+ return;
522
+ }
523
+ const text = String(v ?? "");
524
+ if (el.localName === "textarea") el.textContent = text;
525
+ else if (el.localName !== "select") el.setAttribute("value", text);
526
+ else if (v !== null && v !== void 0) selectOption(el, text, false);
527
+ }
528
+ });
529
+
530
+ //#endregion
531
+ //#region src/mount.ts
532
+ const readSnapshot = (el) => {
533
+ const raw = (el.matches("[data-ilha]") ? el : el.querySelector("[data-ilha]"))?.getAttribute("data-ilha-state");
534
+ if (raw) return decodeSnapshot(raw);
535
+ const first = el.firstElementChild;
536
+ if (first?.matches("template[data-ilha-state]")) {
537
+ first.remove();
538
+ return decodeSnapshot(first.getAttribute("data-ilha-state") ?? "");
539
+ }
540
+ };
541
+ /** Mount with access to the root runtime; `ilha/test` uses it to see pending work. */
542
+ const mountWithRuntime = (el, fn, opts, onRuntime) => {
543
+ const hydrate = opts?.hydrate === true;
544
+ if (!hydrate) el.innerHTML = "";
545
+ const { close } = startRoot({
546
+ fn,
547
+ hydrate,
548
+ onError: opts?.onError,
549
+ onIdle: opts?.onIdle,
550
+ onRuntime,
551
+ paint: paint$1,
552
+ root: el,
553
+ snapshot: hydrate ? readSnapshot(el) : void 0
554
+ });
555
+ return () => {
556
+ close();
557
+ if (!hydrate) el.innerHTML = "";
558
+ };
559
+ };
560
+ const mount = (el, fn, opts) => mountWithRuntime(el, fn, opts);
561
+ const renderToString = async (fn, opts) => {
562
+ const root = new SsrEl("");
563
+ const { close, ready, runtime } = startRoot({
564
+ fn,
565
+ paint,
566
+ root,
567
+ ssr: true
568
+ });
569
+ const { timeout, snapshot = true, markers = true } = opts ?? {};
570
+ if (timeout === void 0) await ready;
571
+ else {
572
+ const late = Promise.withResolvers();
573
+ const timer = setTimeout(late.resolve, timeout, null);
574
+ await Promise.race([ready, late.promise]);
575
+ clearTimeout(timer);
576
+ }
577
+ const inner = serializeNode(root, !markers && !snapshot);
578
+ const state = snapshot ? ` data-ilha-state="${escapeAttr(encodeSnapshot(runtime.ssrValues))}"` : "";
579
+ close();
580
+ if (markers) return `<div data-ilha${state}>${inner}</div>`;
581
+ return state ? `<template${state}></template>${inner}` : inner;
582
+ };
583
+
584
+ //#endregion
585
+ export { mountWithRuntime as n, renderToString as r, mount as t };