ilha 0.15.2 → 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.
@@ -0,0 +1,350 @@
1
+ import { g as makeFiber, i as runSetup, m as closeFiber, n as notifyRoots, r as rootListeners, u as isAtomHandle, y as ilhaWarn } from "./start-B-e1dG5U.js";
2
+ import { a as hasBrand, c as isObject, d as skip, l as isString, n as KEY_ATTR, o as isFunction, p as unwrap, r as SLOT_ATTR, s as isIterable, t as KEEP, u as noop } from "./shared-vpgYXytd.js";
3
+ import { n as isVNode, r as Fragment } from "./vnode-DJ25Jn3D.js";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Stream from "effect/Stream";
6
+
7
+ //#region src/events.ts
8
+ const EVENT_PROP = /^on(?:[A-Z].{2,}|[a-z]{3,}$)/u;
9
+ /** `onClick` / `onclick` → `click`; non-event props → `undefined`. */
10
+ const eventTypeFromProp = (key) => EVENT_PROP.test(key) ? key.slice(2).toLowerCase() : void 0;
11
+ /** One native listener per type; it reads the latest handler at dispatch. */
12
+ const listen = (el, type) => {
13
+ el.__ilhaEvents ??= /* @__PURE__ */ new Set();
14
+ if (el.__ilhaEvents.has(type)) return;
15
+ el.__ilhaEvents.add(type);
16
+ el.addEventListener(type, (ev) => {
17
+ const fiber = el.__ilhaFiber;
18
+ const fn = el.__ilhaHandlers?.get(type);
19
+ if (!fiber || fiber.closed || !isFunction(fn)) return;
20
+ const out = fn(ev);
21
+ if (Effect.isEffect(out)) fiber.run(out);
22
+ });
23
+ };
24
+ const install = (el, handlers, fiber) => {
25
+ el.__ilhaHandlers = handlers;
26
+ el.__ilhaFiber = fiber;
27
+ for (const type of handlers?.keys() ?? []) listen(el, type);
28
+ };
29
+ const bindEvents = (el, props, fiber) => {
30
+ const handlers = /* @__PURE__ */ new Map();
31
+ for (const [key, value] of Object.entries(props)) {
32
+ const type = eventTypeFromProp(key);
33
+ if (type && !handlers.has(type)) handlers.set(type, value);
34
+ }
35
+ install(el, handlers, fiber);
36
+ };
37
+ /**
38
+ * The morph kept `live` in place of the freshly rendered `fresh`: hand the
39
+ * render's handlers to the live node, so closures never go stale and handlers
40
+ * added on a later render still fire.
41
+ */
42
+ const adoptEvents = (live, fresh) => {
43
+ if (fresh.__ilhaFiber) install(live, fresh.__ilhaHandlers, fresh.__ilhaFiber);
44
+ };
45
+
46
+ //#endregion
47
+ //#region src/security.ts
48
+ const SAFE_CSS_PROP = /^-{0,2}[a-zA-Z][a-zA-Z0-9-]*$/u;
49
+ const URL_ATTR = /(?:^|:)(?:href|src|srcset|imagesrcset|action|formaction|cite|data|poster)$/iu;
50
+ const SRCSET_ATTR = /^(?:srcset|imagesrcset)$/iu;
51
+ const SAFE_DATA_IMAGE = /^data:image\/(?:png|jpe?g|gif|webp|avif)[;,]/iu;
52
+ const UNSAFE_SCHEME = /^(?:javascript|vbscript):/iu;
53
+ const URL_PADDING = /[\0-\u0020\u00A0\u1680\u2000-\u200D\u2028\u2029\u202F\u205F\u3000\uFEFF]/gu;
54
+ const UNSAFE_CSS = /[<>{};\\]|expression\(|javascript:|data:/iu;
55
+ const CSS_URL = /url\s*\(\s*(?<inner>[^)]+)\s*\)/giu;
56
+ const isSafeUrl = (value, allowDataImage = false) => {
57
+ const url = value.replaceAll(URL_PADDING, "");
58
+ if (UNSAFE_SCHEME.test(url)) return false;
59
+ return !/^data:/iu.test(url) || allowDataImage && SAFE_DATA_IMAGE.test(url);
60
+ };
61
+ const isUrlAttributeName = (name) => URL_ATTR.test(name);
62
+ const isSafeUrlAttrValue = (tagName, attrName, value) => {
63
+ if (SRCSET_ATTR.test(attrName)) return value.split(",").every((candidate) => isSafeUrl(candidate.trim().split(/\s+/u)[0] ?? ""));
64
+ return isSafeUrl(value, tagName.toLowerCase() === "img" && attrName.toLowerCase() === "src");
65
+ };
66
+ const isSafeStyleValue = (str) => !UNSAFE_CSS.test(str) && [...str.matchAll(CSS_URL)].every((m) => isSafeUrl((m.groups?.inner ?? "").trim().replaceAll(/^['"]|['"]$/gu, "")));
67
+ /** Serialize a style string or object, dropping unsafe properties and values. */
68
+ const serializeStyleAttr = (value) => {
69
+ return (isString(value) ? value.split(";").map((decl) => {
70
+ const colon = decl.indexOf(":");
71
+ return colon === -1 ? ["", ""] : [decl.slice(0, colon).trim(), decl.slice(colon + 1).trim()];
72
+ }) : Object.entries(value)).filter(([k, v]) => k && v !== null && v !== void 0 && SAFE_CSS_PROP.test(k) && isSafeStyleValue(String(v))).map(([k, v]) => `${(k ?? "").replaceAll(/[A-Z]/gu, (m) => `-${m.toLowerCase()}`)}:${String(v)}`).join(";");
73
+ };
74
+
75
+ //#endregion
76
+ //#region src/unsafe.ts
77
+ /**
78
+ * Mark a string as pre-rendered HTML and paint it without escaping.
79
+ * Use it for markup you built or sanitized yourself — SVG from an external
80
+ * library, Markdown rendered to HTML, embed codes. Never pass untrusted
81
+ * user input: unlike interpolated strings, this content is not escaped and
82
+ * scripts inserted this way do not run, but event attributes and other
83
+ * markup still apply.
84
+ */
85
+ const unsafe = (html) => ({
86
+ $$ilhaUnsafe: 1,
87
+ html
88
+ });
89
+ const isUnsafeHtml = (x) => hasBrand(x, "$$ilhaUnsafe");
90
+
91
+ //#endregion
92
+ //#region src/paint-core.ts
93
+ const SVG_NS = "http://www.w3.org/2000/svg";
94
+ const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
95
+ const elementNs = (lower, parent) => {
96
+ if (lower === "svg") return SVG_NS;
97
+ if (lower === "math") return MATHML_NS;
98
+ if (lower === "foreignobject") return "http://www.w3.org/1999/xhtml";
99
+ const ns = parent.namespaceURI;
100
+ return ns === SVG_NS || ns === MATHML_NS ? ns : void 0;
101
+ };
102
+ const hasKey = (view) => view.key !== null && view.key !== void 0;
103
+ /** Shallow equality of two prop bags, children compared item by item. */
104
+ const sameProps = (prev, next) => {
105
+ if (!prev) return false;
106
+ const keys = Object.keys(next);
107
+ return keys.length === Object.keys(prev).length && keys.every((k) => {
108
+ const a = prev[k];
109
+ const b = next[k];
110
+ return k === "children" && Array.isArray(a) && Array.isArray(b) ? a.length === b.length && a.every((x, i) => Object.is(x, b[i])) : Object.is(a, b);
111
+ });
112
+ };
113
+ /** Rerun a component hole with `props`; render closures read the latest box. */
114
+ const rerunHole = (hole, run, props) => {
115
+ hole.propsBox ??= { current: props };
116
+ const box = hole.propsBox;
117
+ box.current = props;
118
+ runSetup(hole, () => run(box.current));
119
+ };
120
+ const hostOf = (fiber) => fiber.root;
121
+ /** Dispose `hole` and drop it from a `filter` pass. */
122
+ const dispose = (hole) => {
123
+ hole.dispose();
124
+ return false;
125
+ };
126
+ const isSkippedPropKey = (k) => k === "children" || k === "key" || k === "ref" || k.toLowerCase() === "srcdoc" || eventTypeFromProp(k) !== void 0 || /^on[A-Z]/u.test(k);
127
+ /**
128
+ * The painter shared by DOM mount, SSR and custom hosts: props,
129
+ * materialization, holes, and keyed reconciliation.
130
+ */
131
+ const createPainter = (ops) => {
132
+ const { committed = (el) => el, reuseNode = (host) => host, setStyle = (el, css) => el.setAttribute("style", css) } = ops;
133
+ const asRoot = (el) => el;
134
+ const self = {};
135
+ let depth = 0;
136
+ const pendingRefs = [];
137
+ const setFormControl = ops.setFormControl ?? ((el, key, v) => {
138
+ if (v === null || v === void 0 || v === false) el.removeAttribute(key);
139
+ else el.setAttribute(key, key === "value" ? String(v) : "");
140
+ });
141
+ const writeAttr = (el, k, v) => {
142
+ if (v === false || v === null || v === void 0) {
143
+ el.removeAttribute(k);
144
+ return;
145
+ }
146
+ const str = v === true ? "" : String(v);
147
+ if (!isUrlAttributeName(k) || isSafeUrlAttrValue(el.tagName, k, str)) el.setAttribute(k, str);
148
+ };
149
+ const applyProp = (el, k, v, fiber) => {
150
+ if (isSkippedPropKey(k) || ops.setProp?.(el, k, v) === true) return;
151
+ if (k === "className" || k === "htmlFor") el.setAttribute(k === "className" ? "class" : "for", String(v ?? ""));
152
+ else if (k === "style") {
153
+ const css = isString(v) || isObject(v) ? serializeStyleAttr(v) : "";
154
+ if (css) setStyle(el, css);
155
+ } else if (k === "value" || k === "checked" || k === "selected") {
156
+ if (isAtomHandle(v)) {
157
+ const unsub = fiber.registry.subscribe(v.atom, (x) => setFormControl(committed(el), k, x), { immediate: true });
158
+ fiber.holes.push({ dispose: unsub });
159
+ } else setFormControl(el, k, v);
160
+ } else if (!isFunction(v)) writeAttr(el, k, v);
161
+ };
162
+ const applyProps = (el, props, fiber) => {
163
+ for (const [k, v] of Object.entries(props)) applyProp(el, k, v, fiber);
164
+ ops.bindEvents?.(el, props, fiber);
165
+ const { ref } = props;
166
+ if (isFunction(ref)) {
167
+ const refFn = ref;
168
+ const attach = () => refFn(committed(el));
169
+ if (depth > 0) pendingRefs.push(attach);
170
+ else attach();
171
+ fiber.holes.push({ dispose: () => refFn(null) });
172
+ }
173
+ };
174
+ /**
175
+ * Open a child fiber rooted in a fresh `display: contents` host. `keep`
176
+ * holes survive repaints that reach them again; others close on repaint.
177
+ */
178
+ const openHole = (parent, meta = {}, onDispose) => {
179
+ parent.runtime.holeSeq += 1;
180
+ const host = ops.createElement("span");
181
+ host.setAttribute(SLOT_ATTR, String(parent.runtime.holeSeq));
182
+ setStyle(host, "display:contents");
183
+ const hole = makeFiber(parent.runtime, asRoot(host), self.paint, { parent });
184
+ hole.seen = parent.gen;
185
+ parent.holes.push({
186
+ ...meta,
187
+ dispose: () => {
188
+ onDispose?.();
189
+ closeFiber(hole);
190
+ },
191
+ fiber: hole
192
+ });
193
+ return {
194
+ hole,
195
+ host
196
+ };
197
+ };
198
+ /** Reuse a kept hole in this paint: mark it reached, return its host. */
199
+ const reuse = (fiber, hole) => {
200
+ hole.seen = fiber.gen;
201
+ return reuseNode(hostOf(hole));
202
+ };
203
+ const materializeAtom = (view, fiber) => {
204
+ const found = fiber.holes.find((h) => h.atom === view.atom && h.fiber && !h.fiber.closed && h.fiber.seen !== fiber.gen && hostOf(h.fiber).isConnected !== false)?.fiber;
205
+ if (found) return [reuse(fiber, found)];
206
+ let unsub;
207
+ const { hole, host } = openHole(fiber, {
208
+ atom: view.atom,
209
+ keep: true
210
+ }, () => unsub?.());
211
+ fiber.runtime.later(() => {
212
+ if (hole.closed) return;
213
+ let last;
214
+ unsub = fiber.registry.subscribe(view.atom, (v) => {
215
+ const next = unwrap(v, last);
216
+ if (next !== KEEP && !hole.closed) {
217
+ last = next;
218
+ self.paint(hole, next);
219
+ }
220
+ }, { immediate: true });
221
+ });
222
+ return [host];
223
+ };
224
+ const materializeStream = (view, fiber) => {
225
+ const { ssr } = fiber.runtime;
226
+ const { hole, host } = openHole(fiber);
227
+ hole.run((ssr ? view.pipe(Stream.take(1)) : view).pipe(Stream.runForEach((v) => Effect.sync(() => self.paint(hole, v))), Effect.tapError((reason) => Effect.sync(() => self.paint(hole, String(reason))))), ssr ? noop : void 0);
228
+ return [host];
229
+ };
230
+ const mountComponent = (fiber, type, props) => {
231
+ const opened = openHole(fiber, { keep: true });
232
+ const { hole } = opened;
233
+ hole.propsBox = { current: props };
234
+ hole.componentType = type;
235
+ fiber.runtime.later(() => {
236
+ if (!hole.closed && hole.renderGen === void 0) rerunHole(hole, type, props);
237
+ });
238
+ return opened;
239
+ };
240
+ const materializeComponent = (view, fiber) => {
241
+ const props = {
242
+ ...view.props,
243
+ children: view.children
244
+ };
245
+ const run = view.type;
246
+ const keyed = fiber.keyedHoles;
247
+ const key = String(view.key);
248
+ let prev = hasKey(view) ? keyed?.get(key) : void 0;
249
+ if (prev && prev.seen === fiber.gen && !prev.closed) {
250
+ ilhaWarn("ILHA005", `key "${key}"`);
251
+ prev = void 0;
252
+ } else if (hasKey(view)) {
253
+ if (!prev || prev.closed || prev.componentType !== run) {
254
+ const { hole, host } = mountComponent(fiber, run, props);
255
+ keyed?.set(key, hole);
256
+ return [host];
257
+ }
258
+ if (!sameProps(prev.propsBox?.current, props)) rerunHole(prev, run, props);
259
+ return [reuse(fiber, prev)];
260
+ }
261
+ fiber.componentFrame ??= {
262
+ i: 0,
263
+ slots: []
264
+ };
265
+ const frame = fiber.componentFrame;
266
+ const at = frame.i;
267
+ frame.i += 1;
268
+ const slot = frame.slots[at];
269
+ if (slot && slot.type === run && !slot.hole.closed) {
270
+ rerunHole(slot.hole, run, props);
271
+ return [reuse(fiber, slot.hole)];
272
+ }
273
+ const { hole, host } = mountComponent(fiber, run, props);
274
+ frame.slots[at] = {
275
+ hole,
276
+ type: run
277
+ };
278
+ return [host];
279
+ };
280
+ const materializeElement = (view, fiber) => {
281
+ const tag = view.type;
282
+ const lower = tag.toLowerCase();
283
+ const el = ops.createElement(tag, elementNs(lower, fiber.root));
284
+ if (hasKey(view)) el.setAttribute(KEY_ATTR, String(view.key));
285
+ const late = lower === "select";
286
+ if (!late) applyProps(el, view.props, fiber);
287
+ const inner = {
288
+ ...fiber,
289
+ root: asRoot(el)
290
+ };
291
+ for (const c of view.children) el.append(...self.materialize(c, inner));
292
+ if (late) applyProps(el, view.props, fiber);
293
+ return [el];
294
+ };
295
+ self.materialize = (view, fiber) => {
296
+ if (skip(view)) return [ops.createText("")];
297
+ if (isUnsafeHtml(view)) return ops.createRaw(view.html, fiber.root);
298
+ if (isAtomHandle(view)) return materializeAtom(view, fiber);
299
+ if (Stream.isStream(view)) return materializeStream(view, fiber);
300
+ if (isFunction(view)) {
301
+ const { hole, host } = openHole(fiber);
302
+ fiber.runtime.later(() => {
303
+ if (!hole.closed) runSetup(hole, view);
304
+ });
305
+ return [host];
306
+ }
307
+ if (isVNode(view)) {
308
+ if (view.type === Fragment) return view.children.flatMap((c) => self.materialize(c, fiber));
309
+ return isFunction(view.type) ? materializeComponent(view, fiber) : materializeElement(view, fiber);
310
+ }
311
+ if (isIterable(view)) return [...view].flatMap((c) => self.materialize(c, fiber));
312
+ return [ops.createText(String(view))];
313
+ };
314
+ self.paint = (fiber, view) => {
315
+ depth += 1;
316
+ try {
317
+ const gen = (fiber.gen ?? 0) + 1;
318
+ fiber.gen = gen;
319
+ fiber.keyedHoles ??= /* @__PURE__ */ new Map();
320
+ fiber.componentFrame ??= {
321
+ i: 0,
322
+ slots: []
323
+ };
324
+ const frame = fiber.componentFrame;
325
+ frame.i = 0;
326
+ fiber.holes = fiber.holes.filter((h) => h.keep || dispose(h));
327
+ const hydrate = fiber.hydrate === true;
328
+ fiber.hydrate = false;
329
+ if (Array.isArray(view) && view.length > 0 && view.every((v) => isVNode(v) && isFunction(v.type) && hasKey(v))) ops.placeChildren(fiber.root, view.flatMap((v) => self.materialize(v, fiber)));
330
+ else {
331
+ const nodes = self.materialize(view, fiber);
332
+ if (ops.morph) ops.morph(fiber.root, nodes, hydrate);
333
+ else ops.placeChildren(fiber.root, nodes);
334
+ }
335
+ frame.slots.length = frame.i;
336
+ fiber.holes = fiber.holes.filter((h) => !h.keep || h.fiber?.seen === gen || dispose(h));
337
+ for (const [k, hole] of fiber.keyedHoles) if (hole.closed) fiber.keyedHoles.delete(k);
338
+ } finally {
339
+ depth -= 1;
340
+ if (depth === 0) {
341
+ for (const run of pendingRefs.splice(0)) run();
342
+ if (rootListeners.size > 0) notifyRoots();
343
+ }
344
+ }
345
+ };
346
+ return self;
347
+ };
348
+
349
+ //#endregion
350
+ export { bindEvents as i, unsafe as n, adoptEvents as r, createPainter as t };
@@ -1,82 +1,55 @@
1
- import { b as View, i as Component, m as PropValue, p as PropBag, u as IlhaRuntime } from "./types-DsdtZMhG.js";
2
- import { n as getFiber, t as FiberLocal } from "./runtime-Baq7MtkE.js";
3
- //#region src/shared.d.ts
4
- declare const KEEP: unique symbol;
5
- //#endregion
1
+ import { _ as View, a as Component, c as IlhaRuntime, m as PropValue, p as PropBag } from "./types-NhEiBvwy.js";
2
+ import { n as getFiber, t as FiberLocal } from "./runtime-BCdKBqYp.js";
6
3
  //#region src/paint-core.d.ts
7
- /** Structural element API shared by DOM `Element` and the SSR `SsrEl` shim. */
4
+ /** Structural element API shared by DOM `Element`, the SSR shim, and custom hosts. */
8
5
  interface PaintEl<Node> {
9
6
  readonly tagName: string;
10
- dataset: DOMStringMap;
7
+ readonly isConnected?: boolean;
11
8
  setAttribute: (name: string, value: string) => void;
12
9
  removeAttribute: (name: string) => void;
13
- appendChild: (node: Node) => void;
14
10
  append: (...nodes: Node[]) => void;
15
11
  }
16
12
  type FormControlKey = "value" | "checked" | "selected";
13
+ /**
14
+ * Host operations behind the shared painter. Fiber roots are the mount root
15
+ * or a hole host element; hosts cast them back to their own node type.
16
+ */
17
17
  interface PaintOps<Node, El extends PaintEl<Node>> {
18
18
  createElement: (tag: string, ns?: string) => El;
19
19
  createText: (text: string) => Node;
20
- /**
21
- * Parse pre-rendered HTML into nodes under `parent` (DOM:
22
- * context-aware fragment parse; SSR: raw node, parent unused).
23
- */
20
+ /** Parse pre-rendered HTML into nodes for `parent`. */
24
21
  createRaw: (html: string, parent: ParentNode) => Node[];
25
- /** Display-contents span marking a dynamic hole; `null` omits the slot attr. */
26
- createSlotHost: (slotId: string | null) => El;
27
- /** Fiber root for a host element (SSR: cast SsrEl up to ParentNode). */
28
- asRoot: (el: El) => ParentNode;
29
- /** Insertable node for a reused fiber/island host. */
30
- asNode: (host: ParentNode) => Node;
31
- /**
32
- * Node a render returns for a reused live host (DOM: a stand-in the morph
33
- * swaps for the host in place, so the host is never detached).
34
- */
35
- reuseNode: (host: ParentNode) => Node;
36
22
  /** Make `root`'s children exactly `nodes`, moving as little as possible. */
37
23
  placeChildren: (root: ParentNode, nodes: Node[]) => void;
38
- /** Append one node to `root`. Only `Painter.insert` calls it. */
39
- appendRoot: (root: ParentNode, node: Node) => void;
40
- /** Element view of a host (SSR: SsrEl masquerades as Element). */
41
- asElement: (el: El) => Element;
42
24
  /**
43
- * The element a rendered element ended up as (DOM: the live node the morph
44
- * kept in its place; SSR: itself).
25
+ * Reconcile `root` with a fresh render, keeping live nodes (DOM morph).
26
+ * Defaults to `placeChildren`. `hydrate` marks a mount's first paint.
45
27
  */
46
- committed: (el: El) => El;
47
- /** Mark a host disconnected (SSR: flag flip; DOM: noop, the DOM tracks it). */
48
- disconnect: (el: El) => void;
28
+ morph?: (root: ParentNode, nodes: Node[], hydrate: boolean) => void;
29
+ /** Node a render returns for a reused host. Defaults to the host itself. */
30
+ reuseNode?: (host: El) => Node;
31
+ /** The element a rendered element ended up as after `morph`. */
32
+ committed?: (el: El) => El;
49
33
  /**
50
34
  * Take a prop as the component wrote it, before it is stringified. Return
51
- * `true` to claim it; anything else falls through to the attribute, style
52
- * and form-control handling below. A host that is not a DOM uses it to keep
53
- * object values. Event props, `children`, `key` and `ref` never reach it.
35
+ * `true` to claim it. Event props, `children`, `key` and `ref` never reach it.
54
36
  */
55
37
  setProp?: (el: El, key: string, value: PropValue) => boolean;
56
- setStyle: (el: El, css: string) => void;
57
- setFormControl: (el: El, key: FormControlKey, v: PropValue) => void;
58
- bindEvents: (el: El, props: PropBag, fiber: FiberLocal) => void;
38
+ /** Defaults to the `style` attribute. */
39
+ setStyle?: (el: El, css: string) => void;
40
+ /** Defaults to attribute writes. */
41
+ setFormControl?: (el: El, key: FormControlKey, v: PropValue) => void;
42
+ bindEvents?: (el: El, props: PropBag, fiber: FiberLocal) => void;
59
43
  }
60
44
  /** Painter surface returned by `createPainter`, shared by every host. */
61
- interface Painter<Node, El> {
62
- applyProps: (el: El, props: PropBag, fiber: FiberLocal) => void;
63
- /** Runs `fn` and flushes queued refs once the outermost paint commits. */
64
- commit: (fn: () => void) => void;
65
- /**
66
- * Runs `fn` as one paint of `fiber`: the keyed components it materializes
67
- * stay alive, and the ones it no longer reaches are closed afterwards.
68
- */
69
- pass: (fiber: FiberLocal, fn: () => void) => void;
70
- disposeHoles: (fiber: FiberLocal, opts?: {
71
- morph?: boolean;
72
- }) => void;
73
- insert: (fiber: FiberLocal, nodes: Node[]) => void;
45
+ interface Painter<Node> {
46
+ /** Paint `view` into `fiber.root`, reusing the holes it still reaches. */
47
+ paint: (fiber: FiberLocal, view: View) => void;
74
48
  materialize: (view: View, fiber: FiberLocal) => Node[];
75
- paintHole: (fiber: FiberLocal, view: View | typeof KEEP) => void;
76
49
  }
77
50
  //#endregion
78
51
  //#region src/events.d.ts
79
- export declare const bindEvents: (node: Element, props: PropBag, fiber: FiberLocal) => void;
52
+ export declare const bindEvents: (el: Element, props: PropBag, fiber: FiberLocal) => void;
80
53
  //#endregion
81
54
  //#region src/renderer.d.ts
82
55
  export interface RendererMountOptions {
@@ -99,21 +72,12 @@ export interface RendererHandle {
99
72
  ready: Promise<null>;
100
73
  runtime: IlhaRuntime;
101
74
  }
102
- export interface HostRenderer<Node, El extends PaintEl<Node> & Node> {
75
+ export interface HostRenderer<Node> {
103
76
  /** The shared painter, for advanced callers that paint holes themselves. */
104
- readonly painter: Painter<Node, El>;
77
+ readonly painter: Painter<Node>;
105
78
  mount: (root: ParentNode, fn: Component, options?: RendererMountOptions) => RendererHandle;
106
79
  }
107
- /**
108
- * Build a renderer for a host described by `ops`.
109
- *
110
- * Unlike the DOM painter this materializes fresh host nodes on every paint and
111
- * attempts no morph: the host receives the new child list through
112
- * `ops.placeChildren` and decides what to keep. Holes that must survive a
113
- * repaint (atoms, islands, keyed rows, positional components) stay alive across
114
- * paints and hand their host node back through `ops.reuseNode`, so their state
115
- * is never remounted.
116
- */
117
- export declare const createRenderer: <Node, El extends PaintEl<Node> & Node>(ops: PaintOps<Node, El>) => HostRenderer<Node, El>;
80
+ /** Build a renderer for a host described by `ops`. */
81
+ export declare const createRenderer: <Node, El extends PaintEl<Node> & Node>(ops: PaintOps<Node, El>) => HostRenderer<Node>;
118
82
  //#endregion
119
83
  export { type Component, type FiberLocal, type FormControlKey, type IlhaRuntime, type PaintEl, type PaintOps, type Painter, type PropBag, type PropValue, type View, getFiber };
package/dist/renderer.js CHANGED
@@ -1,68 +1,36 @@
1
- import { d as defer } from "./vnode-BHUY61Rm.js";
2
- import { C as getFiber, E as makeRuntime, S as closeFiber, T as makeRootFiber, d as runSetup, l as bindEvents, t as createPainter } from "./paint-core-B7jENZwS.js";
1
+ import { a as startRoot, h as getFiber } from "./start-B-e1dG5U.js";
2
+ import { i as bindEvents, t as createPainter } from "./paint-core-D1n4lbpF.js";
3
3
 
4
4
  //#region src/renderer.ts
5
5
  /**
6
6
  * Host renderer seam.
7
7
  *
8
- * `mount()` and `renderToString()` ship the two painters ilha uses on the web:
9
- * the DOM painter and the SSR string painter. This entry exposes the same
10
- * machinery for a third host — a native toolkit, a test harness, or any tree
11
- * that is not a DOM — by letting the caller supply `PaintOps`.
8
+ * `mount()` and `renderToString()` ship the two painters ilha uses on the web.
9
+ * This entry exposes the same painter for a third host — a native toolkit, a
10
+ * test harness, or any tree that is not a DOM — through `PaintOps`.
12
11
  *
13
- * The painter is host-agnostic: props, holes, keyed reconciliation, refs,
14
- * events, atoms and generators all run through `PaintOps`. The only host
15
- * differences are element creation, text creation, placement and teardown.
16
- *
17
- * A host mounted through `createRenderer` is placed with `placeChildren` only:
18
- * every paint hands over the complete child list of the root it painted.
19
- * `appendRoot` is never called on this path.
20
- */
21
- /**
22
- * Build a renderer for a host described by `ops`.
23
- *
24
- * Unlike the DOM painter this materializes fresh host nodes on every paint and
25
- * attempts no morph: the host receives the new child list through
26
- * `ops.placeChildren` and decides what to keep. Holes that must survive a
27
- * repaint (atoms, islands, keyed rows, positional components) stay alive across
28
- * paints and hand their host node back through `ops.reuseNode`, so their state
29
- * is never remounted.
12
+ * Every paint materializes fresh host nodes and hands the complete child list
13
+ * of the painted root to `ops.placeChildren` (or `ops.morph`). Holes that
14
+ * survive a repaint (atoms, keyed rows, positional components) hand their host
15
+ * node back through `ops.reuseNode`, so their state is never remounted.
30
16
  */
17
+ /** Build a renderer for a host described by `ops`. */
31
18
  const createRenderer = (ops) => {
32
19
  const painter = createPainter(ops);
33
- const paint = (fiber, view) => {
34
- painter.commit(() => {
35
- painter.pass(fiber, () => {
36
- fiber.islandFrame ??= {
37
- i: 0,
38
- slots: []
39
- };
40
- painter.disposeHoles(fiber, { morph: true });
41
- ops.placeChildren(fiber.root, painter.materialize(view, fiber));
42
- });
43
- });
44
- };
45
20
  return {
46
- mount(root, fn, options = {}) {
47
- const runtime = makeRuntime({ onError: options.onError });
48
- options.onRuntime?.(runtime);
49
- const { promise: ready, resolve } = defer();
50
- const fiber = makeRootFiber({
51
- paint,
52
- root,
53
- runtime,
54
- settle: resolve
21
+ mount: (root, fn, { onError, onRuntime } = {}) => {
22
+ const { close, ready, runtime } = startRoot({
23
+ fn,
24
+ onError,
25
+ onRuntime,
26
+ paint: painter.paint,
27
+ root
55
28
  });
56
- runtime.begin();
57
- runtime.setIdle(resolve);
58
- runSetup(fiber, fn);
59
- runtime.end();
60
29
  return {
61
30
  ready,
62
31
  runtime,
63
32
  unmount: () => {
64
- closeFiber(fiber);
65
- runtime.close();
33
+ close();
66
34
  ops.placeChildren(root, []);
67
35
  }
68
36
  };