what-core 0.5.5 → 0.6.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.
package/src/dom.js CHANGED
@@ -1,52 +1,11 @@
1
- // What Framework - DOM Reconciler
2
- // Surgical DOM updates. Diff props, diff children, patch only what changed.
3
- // Components use <what-c> wrapper elements (display:contents) for clean reconciliation.
4
- // No virtual DOM tree kept in memory — we diff against the live DOM.
1
+ // What Framework - Fine-Grained DOM Runtime
2
+ // Components run ONCE. Signals create individual DOM effects.
3
+ // No VDOM reconciler, no diffing direct DOM manipulation driven by signals.
5
4
 
6
5
  import { effect, batch, untrack, signal, __DEV__, __devtools } from './reactive.js';
7
6
  import { reportError, _injectGetCurrentComponent, shallowEqual } from './components.js';
8
7
  import { _setComponentRef } from './helpers.js';
9
8
 
10
- // Register <what-c> custom element to prevent flash of unstyled content
11
- // Note: style is set in connectedCallback (not constructor) to comply with custom element spec
12
- if (typeof customElements !== 'undefined' && !customElements.get('what-c')) {
13
- customElements.define('what-c', class extends HTMLElement {
14
- connectedCallback() {
15
- this.style.display = 'contents';
16
- }
17
- // display:contents elements don't generate a layout box — getBoundingClientRect()
18
- // returns zeros, offsetWidth/Height return 0. React libraries (react-draggable,
19
- // react-colorful, etc.) traverse parentNode and call getBoundingClientRect() on
20
- // what they expect to be a layout container. Since <what-c> is layout-invisible,
21
- // delegate to the nearest ancestor that has a real box.
22
- _layoutParent() {
23
- let el = this.parentElement;
24
- while (el && el.tagName === 'WHAT-C') el = el.parentElement;
25
- return el;
26
- }
27
- getBoundingClientRect() {
28
- const p = this._layoutParent();
29
- return p ? p.getBoundingClientRect() : super.getBoundingClientRect();
30
- }
31
- get offsetWidth() {
32
- const p = this._layoutParent();
33
- return p ? p.offsetWidth : 0;
34
- }
35
- get offsetHeight() {
36
- const p = this._layoutParent();
37
- return p ? p.offsetHeight : 0;
38
- }
39
- get clientWidth() {
40
- const p = this._layoutParent();
41
- return p ? p.clientWidth : 0;
42
- }
43
- get clientHeight() {
44
- const p = this._layoutParent();
45
- return p ? p.clientHeight : 0;
46
- }
47
- });
48
- }
49
-
50
9
  // SVG elements that need namespace
51
10
  const SVG_ELEMENTS = new Set([
52
11
  'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
@@ -63,6 +22,9 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
63
22
  // Track all mounted component contexts for disposal
64
23
  const mountedComponents = new Set();
65
24
 
25
+ // WeakMap: comment node → component context (for comment-node boundaries)
26
+ const _commentCtxMap = new WeakMap();
27
+
66
28
  function isDomNode(value) {
67
29
  if (!value || typeof value !== 'object') return false;
68
30
  if (typeof Node !== 'undefined' && value instanceof Node) return true;
@@ -78,24 +40,34 @@ function disposeComponent(ctx) {
78
40
  if (ctx.disposed) return;
79
41
  ctx.disposed = true;
80
42
 
81
- // Run useEffect cleanup functions in reverse order (last effect first, matching React)
82
- for (let i = ctx.hooks.length - 1; i >= 0; i--) {
83
- const hook = ctx.hooks[i];
84
- if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
85
- try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
43
+ // Run cleanup callbacks
44
+ if (ctx.cleanups) {
45
+ for (const cleanup of ctx.cleanups) {
46
+ try { cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
86
47
  }
87
48
  }
88
49
 
89
- // Run onCleanup callbacks in reverse order (last registered first)
90
- if (ctx._cleanupCallbacks) {
91
- for (let i = ctx._cleanupCallbacks.length - 1; i >= 0; i--) {
92
- try { ctx._cleanupCallbacks[i](); } catch (e) { console.error('[what] onCleanup error:', e); }
50
+ // Run effect disposals
51
+ if (ctx.effects) {
52
+ for (const dispose of ctx.effects) {
53
+ try { dispose(); } catch (e) { /* already disposed */ }
54
+ }
55
+ }
56
+
57
+ // Run hook cleanups (useEffect return values)
58
+ if (ctx.hooks) {
59
+ for (const hook of ctx.hooks) {
60
+ if (hook && typeof hook.cleanup === 'function') {
61
+ try { hook.cleanup(); } catch (e) { console.error('[what] hook cleanup error:', e); }
62
+ }
93
63
  }
94
64
  }
95
65
 
96
- // Dispose reactive effects
97
- for (const dispose of ctx.effects) {
98
- try { dispose(); } catch (e) { /* effect already disposed */ }
66
+ // Run onCleanup callbacks
67
+ if (ctx._cleanupCallbacks) {
68
+ for (const fn of ctx._cleanupCallbacks) {
69
+ try { fn(); } catch (e) { console.error('[what] onCleanup error:', e); }
70
+ }
99
71
  }
100
72
 
101
73
  if (__DEV__ && __devtools?.onComponentUnmount) __devtools.onComponentUnmount(ctx);
@@ -108,6 +80,11 @@ export function disposeTree(node) {
108
80
  if (node._componentCtx) {
109
81
  disposeComponent(node._componentCtx);
110
82
  }
83
+ // Check comment node WeakMap for component context
84
+ const commentCtx = _commentCtxMap.get(node);
85
+ if (commentCtx) {
86
+ disposeComponent(commentCtx);
87
+ }
111
88
  // Dispose reactive function child effects ({() => ...} wrappers)
112
89
  if (node._dispose) {
113
90
  try { node._dispose(); } catch (e) { /* already disposed */ }
@@ -153,37 +130,66 @@ export function createDOM(vnode, parent, isSvg) {
153
130
  return document.createTextNode(String(vnode));
154
131
  }
155
132
 
156
- // DOM node passthrough (compiler-first components can return real nodes)
133
+ // DOM node passthrough (fine-grained components return real nodes)
157
134
  if (isDomNode(vnode)) {
158
135
  return vnode;
159
136
  }
160
137
 
161
- // Reactive function child — creates a wrapper that updates fine-grained
162
- // Handles both primitives ({() => count()}) and vnodes ({() => items().map(...)})
138
+ // Reactive function child — use comment markers (no wrapper element)
139
+ // to avoid polluting the DOM and breaking CSS selectors like :first-child.
163
140
  if (typeof vnode === 'function') {
164
- const wrapper = document.createElement('what-c');
165
- let mounted = false;
141
+ const startMarker = document.createComment('fn');
142
+ const endMarker = document.createComment('/fn');
143
+ let currentNodes = [];
144
+ // We need a parent to insert between markers. The caller (createElementFromVNode
145
+ // or createComponent) will appendChild both markers and the content. We return
146
+ // a document fragment containing start marker, then the effect will manage nodes
147
+ // between start and end markers once they're in the real DOM.
148
+ const frag = document.createDocumentFragment();
149
+ frag.appendChild(startMarker);
150
+ frag.appendChild(endMarker);
151
+
166
152
  const dispose = effect(() => {
167
153
  const val = vnode();
168
- // Normalize: null/false/true → empty, primitives and vnodes → array
169
154
  const vnodes = (val == null || val === false || val === true)
170
155
  ? []
171
156
  : Array.isArray(val) ? val : [val];
172
- if (!mounted) {
173
- mounted = true;
174
- for (const v of vnodes) {
175
- const node = createDOM(v, wrapper, parent?._isSvg);
176
- if (node) wrapper.appendChild(node);
157
+
158
+ const realParent = endMarker.parentNode;
159
+ if (!realParent) return; // not mounted yet — first run handled below
160
+
161
+ // Remove old nodes between markers
162
+ for (const old of currentNodes) {
163
+ disposeTree(old);
164
+ if (old.parentNode === realParent) realParent.removeChild(old);
165
+ }
166
+ currentNodes = [];
167
+
168
+ // Add new nodes before endMarker
169
+ for (const v of vnodes) {
170
+ const node = createDOM(v, realParent, parent?._isSvg);
171
+ if (node) {
172
+ // If createDOM returned a DocumentFragment, track individual children
173
+ // since fragment nodes get absorbed into the DOM on insertion.
174
+ if (node.nodeType === 11 /* DOCUMENT_FRAGMENT_NODE */) {
175
+ const children = Array.from(node.childNodes);
176
+ realParent.insertBefore(node, endMarker);
177
+ for (const child of children) currentNodes.push(child);
178
+ } else {
179
+ realParent.insertBefore(node, endMarker);
180
+ currentNodes.push(node);
181
+ }
177
182
  }
178
- } else {
179
- reconcileChildren(wrapper, vnodes);
180
183
  }
181
184
  });
182
- wrapper._dispose = dispose;
183
- return wrapper;
185
+
186
+ startMarker._dispose = dispose;
187
+ // Also store dispose on endMarker so disposeTree can find it from either marker
188
+ endMarker._dispose = dispose;
189
+ return frag;
184
190
  }
185
191
 
186
- // Array (fragment)
192
+ // Array of vnodes
187
193
  if (Array.isArray(vnode)) {
188
194
  const frag = document.createDocumentFragment();
189
195
  for (const child of vnode) {
@@ -193,48 +199,22 @@ export function createDOM(vnode, parent, isSvg) {
193
199
  return frag;
194
200
  }
195
201
 
196
- // Unknown object child fallback
197
- if (!isVNode(vnode)) {
198
- return document.createTextNode(String(vnode));
199
- }
200
-
201
- // Portal (string-tagged vnodes from helpers.js Portal or react-compat createPortal)
202
- if (vnode.tag === '__portal') {
203
- return createPortalDOM(vnode, parent);
204
- }
205
-
206
- // Component
207
- if (typeof vnode.tag === 'function') {
202
+ // VNode with component tag — component runs ONCE
203
+ if (isVNode(vnode) && typeof vnode.tag === 'function') {
208
204
  return createComponent(vnode, parent, isSvg);
209
205
  }
210
206
 
211
- // Detect SVG context: either we're already in SVG, or this tag is an SVG element
212
- const svgContext = isSvg || vnode.tag === 'svg' || SVG_ELEMENTS.has(vnode.tag);
213
-
214
- // HTML or SVG Element
215
- const el = svgContext
216
- ? document.createElementNS(SVG_NS, vnode.tag)
217
- : document.createElement(vnode.tag);
218
-
219
- applyProps(el, vnode.props, {}, svgContext);
220
- const hasRawHtml = vnode.props && (
221
- Object.prototype.hasOwnProperty.call(vnode.props, 'dangerouslySetInnerHTML') ||
222
- Object.prototype.hasOwnProperty.call(vnode.props, 'innerHTML')
223
- );
224
-
225
- if (!hasRawHtml) {
226
- for (const child of vnode.children) {
227
- const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
228
- if (node) el.appendChild(node);
229
- }
207
+ // VNode with string tag create element
208
+ if (isVNode(vnode)) {
209
+ return createElementFromVNode(vnode, parent, isSvg);
230
210
  }
231
211
 
232
- // Store vnode on element for diffing
233
- el._vnode = vnode;
234
- return el;
212
+ // Unknown convert to text
213
+ return document.createTextNode(String(vnode));
235
214
  }
236
215
 
237
216
  // --- Component Rendering ---
217
+ // Components run ONCE. Props are passed as a signal for reactive access.
238
218
 
239
219
  const componentStack = [];
240
220
 
@@ -253,9 +233,7 @@ export function getComponentStack() {
253
233
  function createComponent(vnode, parent, isSvg) {
254
234
  let { tag: Component, props, children } = vnode;
255
235
 
256
- // Class component detection — ES6 classes can't be called without `new`.
257
- // React compat layer wraps class components in createElement, but some
258
- // library-internal components may bypass that path. Detect and wrap here.
236
+ // Class component detection
259
237
  if (typeof Component === 'function' &&
260
238
  (Component.prototype?.isReactComponent || Component.prototype?.render)) {
261
239
  const ClassComp = Component;
@@ -273,7 +251,7 @@ function createComponent(vnode, parent, isSvg) {
273
251
  if (Component === '__suspense' || vnode.tag === '__suspense') {
274
252
  return createSuspenseBoundary(vnode, parent);
275
253
  }
276
- if (Component === '__portal' || vnode.tag === '__portal') { // Now also handled in createDOM directly
254
+ if (Component === '__portal' || vnode.tag === '__portal') {
277
255
  return createPortalDOM(vnode, parent);
278
256
  }
279
257
 
@@ -285,9 +263,8 @@ function createComponent(vnode, parent, isSvg) {
285
263
  cleanups: [],
286
264
  mounted: false,
287
265
  disposed: false,
288
- Component, // Store for identity check in patchNode
266
+ Component,
289
267
  _parentCtx: componentStack[componentStack.length - 1] || null,
290
- // Inherit error boundary from parent context chain
291
268
  _errorBoundary: (() => {
292
269
  let p = componentStack[componentStack.length - 1];
293
270
  while (p) {
@@ -298,82 +275,96 @@ function createComponent(vnode, parent, isSvg) {
298
275
  })()
299
276
  };
300
277
 
301
- // Wrapper element: <what-c display:contents> for HTML, <g> for SVG
302
- // Note: <what-c> custom element sets display:contents in its constructor
303
- let wrapper;
304
- if (isSvg) {
305
- wrapper = document.createElementNS(SVG_NS, 'g');
306
- } else {
307
- wrapper = document.createElement('what-c');
308
- }
309
- wrapper._componentCtx = ctx;
310
- wrapper._isSvg = !!isSvg;
311
- ctx._wrapper = wrapper;
278
+ // Component boundaries: use comment nodes instead of <span style="display:contents">
279
+ // to avoid DOM pollution, CSS selector breakage, and a11y issues.
280
+ const startComment = document.createComment('c:start');
281
+ const endComment = document.createComment('c:end');
282
+ _commentCtxMap.set(startComment, ctx);
283
+ ctx._startComment = startComment;
284
+ ctx._endComment = endComment;
285
+
286
+ // Fragment to hold comment boundaries + component output
287
+ const container = document.createDocumentFragment();
288
+ container._componentCtx = ctx;
289
+ ctx._wrapper = startComment; // Reference for context lookup
312
290
 
313
291
  // Track for disposal
314
292
  mountedComponents.add(ctx);
315
293
  if (__DEV__ && __devtools?.onComponentMount) __devtools.onComponentMount(ctx);
316
294
 
317
295
  // Props signal for reactive updates from parent
318
- // Match React's children semantics: 0→undefined, 1→single child, N→array
319
296
  const propsChildren = children.length === 0 ? undefined : children.length === 1 ? children[0] : children;
320
297
  const propsSignal = signal({ ...props, children: propsChildren });
321
298
  ctx._propsSignal = propsSignal;
322
299
 
323
- // Reactive render: re-renders when signals used inside change
324
- const dispose = effect(() => {
325
- if (ctx.disposed) return;
326
- ctx.hookIndex = 0;
327
-
328
- componentStack.push(ctx);
329
-
330
- let result;
331
- try {
332
- result = Component(propsSignal());
333
- } catch (error) {
334
- componentStack.pop();
335
- if (!reportError(error, ctx)) {
336
- console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
337
- throw error;
300
+ // Create a reactive props proxy: reading any prop inside an effect
301
+ // will auto-track the dependency on the propsSignal. This makes prop
302
+ // access reactive across re-renders without requiring the component
303
+ // to be re-executed.
304
+ const reactiveProps = new Proxy({}, {
305
+ get(_, key) {
306
+ // Access the signal to create a reactive dependency
307
+ const current = propsSignal();
308
+ return current[key];
309
+ },
310
+ has(_, key) {
311
+ const current = propsSignal();
312
+ return key in current;
313
+ },
314
+ ownKeys() {
315
+ const current = propsSignal();
316
+ return Reflect.ownKeys(current);
317
+ },
318
+ getOwnPropertyDescriptor(_, key) {
319
+ const current = propsSignal();
320
+ if (key in current) {
321
+ return { value: current[key], writable: false, enumerable: true, configurable: true };
338
322
  }
339
- return;
340
- }
341
-
342
- // Keep ctx on componentStack while creating/reconciling children
343
- // so child components' _parentCtx correctly points to this component.
344
- // This is essential for context propagation (useContext walks _parentCtx).
323
+ return undefined;
324
+ },
325
+ });
345
326
 
346
- const vnodes = Array.isArray(result) ? result : [result];
327
+ // Component runs ONCE not inside an effect
328
+ componentStack.push(ctx);
347
329
 
348
- if (!ctx.mounted) {
349
- // Initial mount
350
- ctx.mounted = true;
330
+ let result;
331
+ try {
332
+ result = Component(reactiveProps);
333
+ } catch (error) {
334
+ componentStack.pop();
335
+ if (!reportError(error, ctx)) {
336
+ console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
337
+ throw error;
338
+ }
339
+ // Return fragment with just comment boundaries on error
340
+ container.appendChild(startComment);
341
+ container.appendChild(endComment);
342
+ return container;
343
+ }
351
344
 
352
- // Run onMount callbacks after DOM is ready
353
- if (ctx._mountCallbacks) {
354
- queueMicrotask(() => {
355
- if (ctx.disposed) return;
356
- for (const fn of ctx._mountCallbacks) {
357
- try { fn(); } catch (e) { console.error('[what] onMount error:', e); }
358
- }
359
- });
360
- }
345
+ componentStack.pop();
346
+ ctx.mounted = true;
361
347
 
362
- for (const v of vnodes) {
363
- const node = createDOM(v, wrapper, isSvg);
364
- if (node) wrapper.appendChild(node);
348
+ // Run onMount callbacks after DOM is ready
349
+ if (ctx._mountCallbacks) {
350
+ queueMicrotask(() => {
351
+ if (ctx.disposed) return;
352
+ for (const fn of ctx._mountCallbacks) {
353
+ try { fn(); } catch (e) { console.error('[what] onMount error:', e); }
365
354
  }
366
- } else {
367
- // Update: reconcile children inside wrapper
368
- reconcileChildren(wrapper, vnodes);
369
- }
355
+ });
356
+ }
370
357
 
371
- componentStack.pop();
372
- });
358
+ // Build fragment: <!-- c:start --> [component output] <!-- c:end -->
359
+ container.appendChild(startComment);
360
+ const vnodes = Array.isArray(result) ? result : [result];
361
+ for (const v of vnodes) {
362
+ const node = createDOM(v, container, isSvg);
363
+ if (node) container.appendChild(node);
364
+ }
365
+ container.appendChild(endComment);
373
366
 
374
- ctx.effects.push(dispose);
375
- wrapper._vnode = vnode; // Store vnode for keyed reconciliation
376
- return wrapper;
367
+ return container;
377
368
  }
378
369
 
379
370
  // Error boundary component handler
@@ -381,10 +372,9 @@ function createErrorBoundary(vnode, parent) {
381
372
  const { errorState, handleError, fallback, reset } = vnode.props;
382
373
  const children = vnode.children;
383
374
 
384
- const wrapper = document.createElement('what-c');
375
+ const wrapper = document.createElement('span');
385
376
  wrapper.style.display = 'contents';
386
377
 
387
- // Create a boundary context so child components can find this boundary via _parentCtx chain
388
378
  const boundaryCtx = {
389
379
  hooks: [], hookIndex: 0, effects: [], cleanups: [],
390
380
  mounted: false, disposed: false,
@@ -396,9 +386,14 @@ function createErrorBoundary(vnode, parent) {
396
386
  const dispose = effect(() => {
397
387
  const error = errorState();
398
388
 
399
- // Push boundary context so child components inherit _errorBoundary via _parentCtx
400
389
  componentStack.push(boundaryCtx);
401
390
 
391
+ // Remove old content
392
+ while (wrapper.firstChild) {
393
+ disposeTree(wrapper.firstChild);
394
+ wrapper.removeChild(wrapper.firstChild);
395
+ }
396
+
402
397
  let vnodes;
403
398
  if (error) {
404
399
  vnodes = typeof fallback === 'function' ? [fallback({ error, reset })] : [fallback];
@@ -408,13 +403,9 @@ function createErrorBoundary(vnode, parent) {
408
403
 
409
404
  vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
410
405
 
411
- if (wrapper.childNodes.length === 0) {
412
- for (const v of vnodes) {
413
- const node = createDOM(v, wrapper);
414
- if (node) wrapper.appendChild(node);
415
- }
416
- } else {
417
- reconcileChildren(wrapper, vnodes);
406
+ for (const v of vnodes) {
407
+ const node = createDOM(v, wrapper);
408
+ if (node) wrapper.appendChild(node);
418
409
  }
419
410
 
420
411
  componentStack.pop();
@@ -429,10 +420,9 @@ function createSuspenseBoundary(vnode, parent) {
429
420
  const { boundary, fallback, loading } = vnode.props;
430
421
  const children = vnode.children;
431
422
 
432
- const wrapper = document.createElement('what-c');
423
+ const wrapper = document.createElement('span');
433
424
  wrapper.style.display = 'contents';
434
425
 
435
- // Create a boundary context to store the dispose function for cleanup
436
426
  const boundaryCtx = {
437
427
  hooks: [], hookIndex: 0, effects: [], cleanups: [],
438
428
  mounted: false, disposed: false,
@@ -447,13 +437,15 @@ function createSuspenseBoundary(vnode, parent) {
447
437
 
448
438
  componentStack.push(boundaryCtx);
449
439
 
450
- if (wrapper.childNodes.length === 0) {
451
- for (const v of normalized) {
452
- const node = createDOM(v, wrapper);
453
- if (node) wrapper.appendChild(node);
454
- }
455
- } else {
456
- reconcileChildren(wrapper, normalized);
440
+ // Remove old content
441
+ while (wrapper.firstChild) {
442
+ disposeTree(wrapper.firstChild);
443
+ wrapper.removeChild(wrapper.firstChild);
444
+ }
445
+
446
+ for (const v of normalized) {
447
+ const node = createDOM(v, wrapper);
448
+ if (node) wrapper.appendChild(node);
457
449
  }
458
450
 
459
451
  componentStack.pop();
@@ -463,7 +455,7 @@ function createSuspenseBoundary(vnode, parent) {
463
455
  return wrapper;
464
456
  }
465
457
 
466
- // Portal component handler — renders children into a different DOM container
458
+ // Portal component handler
467
459
  function createPortalDOM(vnode, parent) {
468
460
  const { container } = vnode.props;
469
461
  const children = vnode.children;
@@ -473,18 +465,15 @@ function createPortalDOM(vnode, parent) {
473
465
  return document.createComment('portal:empty');
474
466
  }
475
467
 
476
- // Create a boundary context for cleanup
477
468
  const portalCtx = {
478
469
  hooks: [], hookIndex: 0, effects: [], cleanups: [],
479
470
  mounted: false, disposed: false,
480
471
  _parentCtx: componentStack[componentStack.length - 1] || null,
481
472
  };
482
473
 
483
- // Placeholder in the original tree for reconciliation
484
474
  const placeholder = document.createComment('portal');
485
475
  placeholder._componentCtx = portalCtx;
486
476
 
487
- // Render children into the target container
488
477
  const portalNodes = [];
489
478
  for (const child of children) {
490
479
  const node = createDOM(child, container);
@@ -494,7 +483,6 @@ function createPortalDOM(vnode, parent) {
494
483
  }
495
484
  }
496
485
 
497
- // Register cleanup to remove portal nodes when placeholder is disposed
498
486
  portalCtx._cleanupCallbacks = [() => {
499
487
  for (const node of portalNodes) {
500
488
  disposeTree(node);
@@ -505,513 +493,57 @@ function createPortalDOM(vnode, parent) {
505
493
  return placeholder;
506
494
  }
507
495
 
508
- // --- Reconciliation ---
509
- // Diff old DOM nodes against new VNodes, patch in place.
510
- // Uses keyed reconciliation with LIS (Longest Increasing Subsequence) for minimal DOM moves.
511
-
512
- function reconcile(parent, oldNodes, newVNodes, beforeMarker) {
513
- if (!parent) return;
514
-
515
- const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
516
-
517
- if (hasKeys) {
518
- reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
519
- } else {
520
- reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
521
- }
522
- }
523
-
524
- // Unkeyed reconciliation (index-based, fast for static lists)
525
- function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
526
- const maxLen = Math.max(oldNodes.length, newVNodes.length);
527
- const newNodes = [];
528
-
529
- for (let i = 0; i < maxLen; i++) {
530
- const oldNode = oldNodes[i];
531
- const newVNode = newVNodes[i];
532
-
533
- if (i >= newVNodes.length) {
534
- // Remove extra old nodes
535
- if (oldNode && oldNode.parentNode) {
536
- disposeTree(oldNode);
537
- oldNode.parentNode.removeChild(oldNode);
538
- }
539
- continue;
540
- }
541
-
542
- if (i >= oldNodes.length) {
543
- // Append new nodes
544
- const node = createDOM(newVNode, parent);
545
- if (node) {
546
- const ref = getInsertionRef(oldNodes, beforeMarker);
547
- parent.insertBefore(node, ref);
548
- newNodes.push(node);
549
- }
550
- continue;
551
- }
552
-
553
- // Patch existing node
554
- const patched = patchNode(parent, oldNode, newVNode);
555
- newNodes.push(patched);
556
- }
557
-
558
- // Update the reference array
559
- oldNodes.length = 0;
560
- oldNodes.push(...newNodes);
561
- }
562
-
563
- // Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
564
- function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
565
- const newLen = newVNodes.length;
566
- const oldLen = oldNodes.length;
567
-
568
- // --- Fast path: same-position keys (covers "update N items in-place") ---
569
- // If same length and all keys match at the same index, skip LIS entirely.
570
- // Just patch each node in-place — O(n) with zero DOM moves.
571
- if (newLen === oldLen && newLen > 0) {
572
- let allMatch = true;
573
- for (let i = 0; i < newLen; i++) {
574
- const newKey = newVNodes[i]?.key;
575
- const oldKey = oldNodes[i]?._vnode?.key;
576
- if (newKey == null || newKey !== oldKey) {
577
- allMatch = false;
578
- break;
579
- }
580
- }
581
- if (allMatch) {
582
- for (let i = 0; i < newLen; i++) {
583
- patchNode(parent, oldNodes[i], newVNodes[i]);
584
- }
585
- return;
586
- }
587
- }
588
-
589
- // Build old key -> { node, index } map
590
- const oldKeyMap = new Map();
591
- for (let i = 0; i < oldLen; i++) {
592
- const node = oldNodes[i];
593
- const key = node._vnode?.key;
594
- if (key != null) {
595
- oldKeyMap.set(key, { node, index: i });
596
- }
597
- }
598
-
599
- const newNodes = [];
600
-
601
- // First pass: match keys and find reusable nodes
602
- const sources = new Array(newLen).fill(-1); // Maps new index to old index
603
- const reused = new Set();
604
-
605
- for (let i = 0; i < newLen; i++) {
606
- const vnode = newVNodes[i];
607
- const key = vnode?.key;
608
- if (key != null && oldKeyMap.has(key)) {
609
- const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
610
- sources[i] = oldIndex;
611
- reused.add(oldIndex);
612
- }
613
- }
614
-
615
- // Remove nodes that aren't reused
616
- for (let i = 0; i < oldLen; i++) {
617
- if (!reused.has(i) && oldNodes[i]?.parentNode) {
618
- disposeTree(oldNodes[i]);
619
- oldNodes[i].parentNode.removeChild(oldNodes[i]);
620
- }
621
- }
622
-
623
- // Find LIS (Longest Increasing Subsequence) of old indices.
624
- // The LIS tells us which reused nodes are already in correct relative order
625
- // and don't need to be moved. Only nodes NOT in the LIS need DOM moves.
626
- //
627
- // Step 1: Filter out -1 entries (new nodes with no old counterpart).
628
- // Step 2: Compute LIS on the filtered array. Result: indices into the filtered array.
629
- // Step 3: Map filtered-array indices back to original sources[] indices (new-VNode indices).
630
- // For each LIS index `lis[i]`, we find the `lis[i]`-th non-negative entry in sources[]
631
- // and return its position in the original sources array.
632
- // Build filteredToOriginal map in one O(n) pass instead of O(n²) nested loop
633
- const filtered = [];
634
- const filteredToOriginal = [];
635
- for (let j = 0; j < sources.length; j++) {
636
- if (sources[j] !== -1) {
637
- filteredToOriginal.push(j);
638
- filtered.push(sources[j]);
639
- }
640
- }
641
- const lis = longestIncreasingSubsequence(filtered);
642
- const lisSet = new Set(lis.map(i => filteredToOriginal[i]));
643
-
644
- // Build new nodes array and move/create as needed
645
- let lastInserted = beforeMarker?.nextSibling || null;
646
-
647
- // Process in reverse order for correct insertion
648
- for (let i = newLen - 1; i >= 0; i--) {
649
- const vnode = newVNodes[i];
650
- const key = vnode?.key;
651
- const oldEntry = key != null ? oldKeyMap.get(key) : null;
652
-
653
- if (oldEntry && sources[i] !== -1) {
654
- // Reuse existing node
655
- const oldNode = oldEntry.node;
656
- // Patch props/children
657
- const patched = patchNode(parent, oldNode, vnode);
658
- newNodes[i] = patched;
659
-
660
- // Move if not in LIS
661
- if (!lisSet.has(i) && patched.parentNode) {
662
- parent.insertBefore(patched, lastInserted);
663
- }
664
- lastInserted = patched;
665
- } else {
666
- // Create new node
667
- const node = createDOM(vnode, parent);
668
- if (node) {
669
- parent.insertBefore(node, lastInserted);
670
- lastInserted = node;
671
- }
672
- newNodes[i] = node;
673
- }
674
- }
675
-
676
- // Update the reference array
677
- oldNodes.length = 0;
678
- oldNodes.push(...newNodes.filter(Boolean));
679
- }
680
-
681
- // Longest Increasing Subsequence - O(n log n)
682
- // Returns indices of elements that form the LIS
683
- function longestIncreasingSubsequence(arr) {
684
- if (arr.length === 0) return [];
685
-
686
- const n = arr.length;
687
- const dp = new Array(n).fill(1); // Length of LIS ending at i
688
- const parent = new Array(n).fill(-1); // Parent index for reconstruction
689
- const tails = [0]; // Indices of smallest tail elements
690
-
691
- for (let i = 1; i < n; i++) {
692
- if (arr[i] > arr[tails[tails.length - 1]]) {
693
- parent[i] = tails[tails.length - 1];
694
- tails.push(i);
695
- } else {
696
- // Binary search for the smallest element >= arr[i]
697
- let lo = 0, hi = tails.length - 1;
698
- while (lo < hi) {
699
- const mid = (lo + hi) >> 1;
700
- if (arr[tails[mid]] < arr[i]) lo = mid + 1;
701
- else hi = mid;
702
- }
703
- if (arr[i] < arr[tails[lo]]) {
704
- if (lo > 0) parent[i] = tails[lo - 1];
705
- tails[lo] = i;
706
- }
707
- }
708
- }
709
-
710
- // Reconstruct LIS
711
- const result = [];
712
- let k = tails[tails.length - 1];
713
- while (k !== -1) {
714
- result.push(k);
715
- k = parent[k];
716
- }
717
- return result.reverse();
718
- }
719
-
720
- function getInsertionRef(nodes, marker) {
721
- if (nodes.length > 0) {
722
- const last = nodes[nodes.length - 1];
723
- return last.nextSibling;
724
- }
725
- return marker ? marker.nextSibling : null;
726
- }
496
+ // --- Create Element from VNode ---
497
+ // For h()-based VNodes with string tags
727
498
 
728
- // Helper: clean up array marker range (startMarker .. endMarker) and return a clean replacement node
729
- function cleanupArrayMarkers(parent, startMarker) {
730
- const endMarker = startMarker._arrayEnd;
731
- if (!endMarker) return null;
732
- // Remove all nodes between start and end markers
733
- let node = startMarker.nextSibling;
734
- while (node && node !== endMarker) {
735
- const next = node.nextSibling;
736
- disposeTree(node);
737
- parent.removeChild(node);
738
- node = next;
739
- }
740
- // Remove end marker
741
- if (endMarker.parentNode) parent.removeChild(endMarker);
742
- return startMarker;
743
- }
499
+ function createElementFromVNode(vnode, parent, isSvg) {
500
+ const { tag, props, children } = vnode;
744
501
 
745
- function patchNode(parent, domNode, vnode) {
746
- // Null/removed keep placeholder or replace with one
747
- if (vnode == null || vnode === false || vnode === true) {
748
- // Handle array marker cleanup
749
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
750
- cleanupArrayMarkers(parent, domNode);
751
- const placeholder = document.createComment('');
752
- parent.replaceChild(placeholder, domNode);
753
- return placeholder;
754
- }
755
- if (domNode && domNode.nodeType === 8 && !domNode._componentCtx) {
756
- return domNode; // already a placeholder comment
757
- }
758
- const placeholder = document.createComment('');
759
- if (domNode && domNode.parentNode) {
760
- disposeTree(domNode);
761
- parent.replaceChild(placeholder, domNode);
762
- }
763
- return placeholder;
764
- }
765
-
766
- // Reactive function child — replace whatever's there with a reactive wrapper
767
- if (typeof vnode === 'function') {
768
- const wrapper = document.createElement('what-c');
769
- let mounted = false;
770
- const dispose = effect(() => {
771
- const val = vnode();
772
- const vnodes = (val == null || val === false || val === true)
773
- ? []
774
- : Array.isArray(val) ? val : [val];
775
- if (!mounted) {
776
- mounted = true;
777
- for (const v of vnodes) {
778
- const node = createDOM(v, wrapper);
779
- if (node) wrapper.appendChild(node);
780
- }
781
- } else {
782
- reconcileChildren(wrapper, vnodes);
783
- }
784
- });
785
- wrapper._dispose = dispose;
786
- if (domNode && domNode.parentNode) {
787
- disposeTree(domNode);
788
- parent.replaceChild(wrapper, domNode);
789
- }
790
- return wrapper;
791
- }
792
-
793
- // DOM node passthrough
794
- if (isDomNode(vnode)) {
795
- if (domNode === vnode) return domNode;
796
- if (domNode && domNode.parentNode) {
797
- disposeTree(domNode);
798
- parent.replaceChild(vnode, domNode);
799
- }
800
- return vnode;
801
- }
802
-
803
- // Text
804
- if (typeof vnode === 'string' || typeof vnode === 'number') {
805
- const text = String(vnode);
806
- // Clean up array markers if transitioning from array to text
807
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
808
- cleanupArrayMarkers(parent, domNode);
809
- const newNode = document.createTextNode(text);
810
- parent.replaceChild(newNode, domNode);
811
- return newNode;
812
- }
813
- if (domNode.nodeType === 3) {
814
- if (domNode.textContent !== text) domNode.textContent = text;
815
- return domNode;
816
- }
817
- const newNode = document.createTextNode(text);
818
- disposeTree(domNode);
819
- parent.replaceChild(newNode, domNode);
820
- return newNode;
821
- }
822
-
823
- // Array — use marker comments to bracket the range (DocumentFragment empties on append)
824
- if (Array.isArray(vnode)) {
825
- // If domNode is already an array marker, reconcile contents in place
826
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
827
- const endMarker = domNode._arrayEnd;
828
- // Collect existing children between markers
829
- const oldChildren = [];
830
- let node = domNode.nextSibling;
831
- while (node && node !== endMarker) {
832
- oldChildren.push(node);
833
- node = node.nextSibling;
834
- }
835
- // Reconcile the array contents
836
- const maxLen = Math.max(oldChildren.length, vnode.length);
837
- for (let i = 0; i < maxLen; i++) {
838
- if (i >= vnode.length) {
839
- // Remove extra old nodes
840
- if (oldChildren[i]?.parentNode) {
841
- disposeTree(oldChildren[i]);
842
- parent.removeChild(oldChildren[i]);
843
- }
844
- } else if (i >= oldChildren.length) {
845
- // Append new nodes before end marker
846
- const newNode = createDOM(vnode[i], parent);
847
- if (newNode) parent.insertBefore(newNode, endMarker);
848
- } else {
849
- // Patch existing
850
- patchNode(parent, oldChildren[i], vnode[i]);
851
- }
852
- }
853
- return domNode;
854
- }
855
- // Fresh array: create markers
856
- const startMarker = document.createComment('[');
857
- const endMarker = document.createComment(']');
858
- disposeTree(domNode);
859
- parent.replaceChild(endMarker, domNode);
860
- parent.insertBefore(startMarker, endMarker);
861
- for (const v of vnode) {
862
- const node = createDOM(v, parent);
863
- if (node) parent.insertBefore(node, endMarker);
864
- }
865
- startMarker._arrayEnd = endMarker;
866
- return startMarker;
867
- }
868
-
869
- // Unknown object child fallback
870
- if (!isVNode(vnode)) {
871
- const text = String(vnode);
872
- if (domNode.nodeType === 3) {
873
- if (domNode.textContent !== text) domNode.textContent = text;
874
- return domNode;
875
- }
876
- const newNode = document.createTextNode(text);
877
- disposeTree(domNode);
878
- parent.replaceChild(newNode, domNode);
879
- return newNode;
880
- }
502
+ const svgContext = isSvg || SVG_ELEMENTS.has(tag);
503
+ const el = svgContext
504
+ ? document.createElementNS(SVG_NS, tag)
505
+ : document.createElement(tag);
881
506
 
882
- // Component
883
- if (typeof vnode.tag === 'function') {
884
- // Check if old node is a component wrapper for the same component
885
- if (domNode._componentCtx && !domNode._componentCtx.disposed
886
- && domNode._componentCtx.Component === vnode.tag) {
887
- // Same component — update props reactively, let its effect re-render
888
- const ch = vnode.children;
889
- const patchChildren = ch.length === 0 ? undefined : ch.length === 1 ? ch[0] : ch;
890
- const nextProps = { ...vnode.props, children: patchChildren };
891
- // Skip signal update if props haven't changed (shallow compare)
892
- const prevProps = domNode._componentCtx._propsSignal.peek();
893
- if (!shallowEqual(prevProps, nextProps)) {
894
- domNode._componentCtx._propsSignal.set(nextProps);
895
- }
896
- domNode._vnode = vnode; // Keep vnode current for keyed reconciliation
897
- return domNode;
898
- }
899
- // Different component or not a component — dispose old, create new
900
- disposeTree(domNode);
901
- const node = createComponent(vnode, parent);
902
- parent.replaceChild(node, domNode);
903
- return node;
507
+ // Apply props
508
+ if (props) {
509
+ applyProps(el, props, {}, svgContext);
904
510
  }
905
511
 
906
- // Element: same tag? Patch props + children
907
- if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
908
- const oldProps = domNode._vnode?.props || {};
909
- const nextProps = vnode.props || {};
910
- const hadRawHtml = Object.prototype.hasOwnProperty.call(oldProps, 'dangerouslySetInnerHTML')
911
- || Object.prototype.hasOwnProperty.call(oldProps, 'innerHTML');
912
- const hasRawHtml = Object.prototype.hasOwnProperty.call(nextProps, 'dangerouslySetInnerHTML')
913
- || Object.prototype.hasOwnProperty.call(nextProps, 'innerHTML');
914
-
915
- // If switching from normal children to raw HTML, dispose existing child effects first.
916
- if (hasRawHtml && !hadRawHtml) {
917
- for (const child of Array.from(domNode.childNodes)) {
918
- disposeTree(child);
919
- }
920
- }
921
-
922
- applyProps(domNode, nextProps, oldProps);
923
-
924
- // Raw HTML props own the element's children. Skip vnode child reconciliation.
925
- if (!hasRawHtml) {
926
- reconcileChildren(domNode, vnode.children);
927
- }
928
-
929
- domNode._vnode = vnode;
930
- return domNode;
512
+ // Append children
513
+ for (const child of children) {
514
+ const node = createDOM(child, el, svgContext && tag !== 'foreignObject');
515
+ if (node) el.appendChild(node);
931
516
  }
932
517
 
933
- // Different tag: replace entirely
934
- const newNode = createDOM(vnode, parent);
935
- disposeTree(domNode);
936
- parent.replaceChild(newNode, domNode);
937
- return newNode;
938
- }
939
-
940
- function reconcileChildren(parent, newChildVNodes) {
941
- const oldChildren = Array.from(parent.childNodes);
942
-
943
- // Check for keyed children
944
- const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
945
-
946
- if (hasKeys) {
947
- // Use keyed reconciliation
948
- reconcileKeyed(parent, oldChildren, newChildVNodes, null);
949
- } else {
950
- // Unkeyed reconciliation
951
- const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
952
-
953
- for (let i = 0; i < maxLen; i++) {
954
- if (i >= newChildVNodes.length) {
955
- // Remove extra
956
- if (oldChildren[i]?.parentNode) {
957
- disposeTree(oldChildren[i]);
958
- parent.removeChild(oldChildren[i]);
959
- }
960
- continue;
961
- }
962
-
963
- if (i >= oldChildren.length) {
964
- // Append new
965
- const node = createDOM(newChildVNodes[i], parent);
966
- if (node) parent.appendChild(node);
967
- continue;
968
- }
969
-
970
- patchNode(parent, oldChildren[i], newChildVNodes[i]);
971
- }
972
- }
518
+ el._vnode = vnode;
519
+ return el;
973
520
  }
974
521
 
975
- // --- Prop Diffing ---
976
- // Only touch DOM for props that actually changed.
522
+ // --- Prop Application ---
523
+ // Only applied once for fine-grained (no diffing). Reactive props use effects.
977
524
 
978
525
  function applyProps(el, newProps, oldProps, isSvg) {
979
526
  newProps = newProps || {};
980
527
  oldProps = oldProps || {};
981
528
 
982
- // Remove old props not in new
983
- for (const key in oldProps) {
984
- if (key === 'key' || key === 'ref' || key === 'children') continue;
985
- if (!(key in newProps)) {
986
- removeProp(el, key, oldProps[key]);
987
- }
988
- }
989
-
990
- // Set new/changed props
991
529
  for (const key in newProps) {
992
- if (key === 'key' || key === 'ref' || key === 'children') continue;
993
- if (newProps[key] !== oldProps[key]) {
994
- setProp(el, key, newProps[key], isSvg);
530
+ if (key === 'key' || key === 'children') continue;
531
+
532
+ // Handle ref
533
+ if (key === 'ref') {
534
+ if (typeof newProps.ref === 'function') newProps.ref(el);
535
+ else if (newProps.ref) newProps.ref.current = el;
536
+ continue;
995
537
  }
996
- }
997
538
 
998
- // Handle ref
999
- if (newProps.ref && newProps.ref !== oldProps.ref) {
1000
- if (typeof newProps.ref === 'function') newProps.ref(el);
1001
- else newProps.ref.current = el;
539
+ setProp(el, key, newProps[key], isSvg);
1002
540
  }
1003
541
  }
1004
542
 
1005
543
  function setProp(el, key, value, isSvg) {
1006
- // Reactive function props — wrap in effect() for fine-grained updates.
1007
- // Applies to any non-event prop where the value is a function, e.g.:
1008
- // h('input', { value: () => name(), class: () => active() ? 'on' : 'off' })
1009
- // The function is called inside an effect, so signal reads create subscriptions.
1010
- // When signals change, the prop is re-applied automatically.
544
+ // Reactive function props — wrap in effect for fine-grained updates
1011
545
  if (typeof value === 'function' && !(key.startsWith('on') && key.length > 2) && key !== 'ref') {
1012
- // Store dispose functions on the element for cleanup
1013
546
  if (!el._propEffects) el._propEffects = {};
1014
- // Dispose previous effect for this prop if re-applying
1015
547
  if (el._propEffects[key]) {
1016
548
  try { el._propEffects[key](); } catch (e) { /* already disposed */ }
1017
549
  }
@@ -1022,38 +554,27 @@ function setProp(el, key, value, isSvg) {
1022
554
  return;
1023
555
  }
1024
556
 
1025
- // Event handlers: onClick -> click, onFocusCapture -> focus (capture phase)
1026
- // Wrap in untrack so signal reads in handlers don't create subscriptions
557
+ // Event handlers
1027
558
  if (key.startsWith('on') && key.length > 2) {
1028
559
  let eventName = key.slice(2);
1029
- // React-style capture phase: onClickCapture → click in capture phase
1030
560
  let useCapture = false;
1031
561
  if (eventName.endsWith('Capture')) {
1032
562
  eventName = eventName.slice(0, -7);
1033
563
  useCapture = true;
1034
564
  }
1035
565
  const event = eventName.toLowerCase();
1036
- // Use a combined key for storage so capture/bubble don't conflict
1037
566
  const storageKey = useCapture ? event + '_capture' : event;
1038
- // Store handler for removal
1039
567
  const old = el._events?.[storageKey];
1040
- // Skip re-wrapping if same handler function
1041
568
  if (old && old._original === value) return;
1042
569
  if (old) el.removeEventListener(event, old, useCapture);
1043
- // If handler is null/undefined, just remove the old one and bail
1044
570
  if (value == null) return;
1045
571
  if (!el._events) el._events = {};
1046
- // Wrap handler to untrack signal reads.
1047
- // Add nativeEvent for React compat — React synthetic events have
1048
- // e.nativeEvent pointing to the actual DOM event. Libraries like
1049
- // react-colorful, cmdk, and @floating-ui/react check this property.
1050
572
  const wrappedHandler = (e) => {
1051
573
  if (!e.nativeEvent) e.nativeEvent = e;
1052
574
  return untrack(() => value(e));
1053
575
  };
1054
576
  wrappedHandler._original = value;
1055
577
  el._events[storageKey] = wrappedHandler;
1056
- // Check for _eventOpts (once/capture/passive from compiler)
1057
578
  const eventOpts = value._eventOpts;
1058
579
  el.addEventListener(event, wrappedHandler, eventOpts || useCapture || undefined);
1059
580
  return;
@@ -1069,13 +590,12 @@ function setProp(el, key, value, isSvg) {
1069
590
  return;
1070
591
  }
1071
592
 
1072
- // Style object — track previous style to remove stale properties
593
+ // Style
1073
594
  if (key === 'style') {
1074
595
  if (typeof value === 'string') {
1075
596
  el.style.cssText = value;
1076
597
  el._prevStyle = null;
1077
598
  } else if (typeof value === 'object') {
1078
- // Remove old style properties not in new style
1079
599
  const oldStyle = el._prevStyle || {};
1080
600
  for (const prop in oldStyle) {
1081
601
  if (!(prop in value)) el.style[prop] = '';
@@ -1094,12 +614,20 @@ function setProp(el, key, value, isSvg) {
1094
614
  return;
1095
615
  }
1096
616
 
1097
- // innerHTML convenience alias
617
+ // innerHTML require { __html: ... } wrapper to prevent XSS
1098
618
  if (key === 'innerHTML') {
619
+ if (value == null) return; // null/undefined — do nothing
1099
620
  if (value && typeof value === 'object' && '__html' in value) {
1100
621
  el.innerHTML = value.__html ?? '';
1101
622
  } else {
1102
- el.innerHTML = value ?? '';
623
+ if (__DEV__) {
624
+ console.warn(
625
+ '[what] innerHTML received a raw string. This is a security risk (XSS). ' +
626
+ 'Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead.'
627
+ );
628
+ }
629
+ // Refuse to set raw string innerHTML — prevent XSS
630
+ return;
1103
631
  }
1104
632
  return;
1105
633
  }
@@ -1111,13 +639,13 @@ function setProp(el, key, value, isSvg) {
1111
639
  return;
1112
640
  }
1113
641
 
1114
- // data-* and aria-* as attributes
642
+ // data-* and aria-*
1115
643
  if (key.startsWith('data-') || key.startsWith('aria-')) {
1116
644
  el.setAttribute(key, value);
1117
645
  return;
1118
646
  }
1119
647
 
1120
- // SVG: always use setAttribute (SVG properties don't work as DOM properties)
648
+ // SVG
1121
649
  if (isSvg) {
1122
650
  if (value === false || value == null) {
1123
651
  el.removeAttribute(key);
@@ -1127,46 +655,10 @@ function setProp(el, key, value, isSvg) {
1127
655
  return;
1128
656
  }
1129
657
 
1130
- // Default: set as property if it exists, otherwise attribute
658
+ // Default: property if exists, otherwise attribute
1131
659
  if (key in el) {
1132
660
  el[key] = value;
1133
661
  } else {
1134
662
  el.setAttribute(key, value);
1135
663
  }
1136
664
  }
1137
-
1138
- function removeProp(el, key, oldValue) {
1139
- if (key.startsWith('on') && key.length > 2) {
1140
- let eventName = key.slice(2);
1141
- let useCapture = false;
1142
- if (eventName.endsWith('Capture')) {
1143
- eventName = eventName.slice(0, -7);
1144
- useCapture = true;
1145
- }
1146
- const event = eventName.toLowerCase();
1147
- const storageKey = useCapture ? event + '_capture' : event;
1148
- if (el._events?.[storageKey]) {
1149
- el.removeEventListener(event, el._events[storageKey], useCapture);
1150
- delete el._events[storageKey];
1151
- }
1152
- return;
1153
- }
1154
-
1155
- if (key === 'className' || key === 'class') {
1156
- el.className = '';
1157
- return;
1158
- }
1159
-
1160
- if (key === 'style') {
1161
- el.style.cssText = '';
1162
- el._prevStyle = null;
1163
- return;
1164
- }
1165
-
1166
- if (key === 'dangerouslySetInnerHTML' || key === 'innerHTML') {
1167
- el.innerHTML = '';
1168
- return;
1169
- }
1170
-
1171
- el.removeAttribute(key);
1172
- }