what-core 0.1.1 → 0.3.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/dist/dom.js CHANGED
@@ -1,443 +1,718 @@
1
- import { effect, batch, untrack } from './reactive.js';
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.
5
+
6
+ import { effect, batch, untrack, signal } from './reactive.js';
2
7
  import { errorBoundaryStack, reportError } from './components.js';
8
+
9
+ // SVG elements that need namespace
10
+ const SVG_ELEMENTS = new Set([
11
+ 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
12
+ 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern', 'image',
13
+ 'text', 'tspan', 'textPath', 'foreignObject', 'linearGradient', 'radialGradient', 'stop',
14
+ 'marker', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter',
15
+ 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
16
+ 'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage',
17
+ 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting',
18
+ 'feTile', 'feTurbulence',
19
+ ]);
20
+ const SVG_NS = 'http://www.w3.org/2000/svg';
21
+
22
+ // Track all mounted component contexts for disposal
23
+ const mountedComponents = new Set();
24
+
25
+ // Dispose a component: run effect cleanups, hook cleanups, onCleanup callbacks
26
+ function disposeComponent(ctx) {
27
+ if (ctx.disposed) return;
28
+ ctx.disposed = true;
29
+
30
+ // Run useEffect cleanup functions
31
+ for (const hook of ctx.hooks) {
32
+ if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
33
+ try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
34
+ }
35
+ }
36
+
37
+ // Run onCleanup callbacks
38
+ if (ctx._cleanupCallbacks) {
39
+ for (const fn of ctx._cleanupCallbacks) {
40
+ try { fn(); } catch (e) { console.error('[what] onCleanup error:', e); }
41
+ }
42
+ }
43
+
44
+ // Dispose reactive effects
45
+ for (const dispose of ctx.effects) {
46
+ try { dispose(); } catch (e) { /* effect already disposed */ }
47
+ }
48
+
49
+ mountedComponents.delete(ctx);
50
+ }
51
+
52
+ // Dispose all components attached to a DOM subtree
53
+ function disposeTree(node) {
54
+ if (!node) return;
55
+ if (node._componentCtx) {
56
+ disposeComponent(node._componentCtx);
57
+ }
58
+ if (node.childNodes) {
59
+ for (const child of node.childNodes) {
60
+ disposeTree(child);
61
+ }
62
+ }
63
+ }
64
+
65
+ // Mount a component tree into a DOM container
3
66
  export function mount(vnode, container) {
4
- if (typeof container === 'string') {
5
- container = document.querySelector(container);
6
- }
7
- container.textContent = '';
8
- const node = createDOM(vnode, container);
9
- if (node) container.appendChild(node);
10
- return () => {
11
- container.textContent = '';
12
- };
13
- }
14
- function createDOM(vnode, parent) {
15
- if (vnode == null || vnode === false || vnode === true) return null;
16
- if (typeof vnode === 'string' || typeof vnode === 'number') {
17
- return document.createTextNode(String(vnode));
18
- }
19
- if (Array.isArray(vnode)) {
20
- const frag = document.createDocumentFragment();
21
- for (const child of vnode) {
22
- const node = createDOM(child, parent);
23
- if (node) frag.appendChild(node);
24
- }
25
- return frag;
26
- }
27
- if (typeof vnode.tag === 'function') {
28
- return createComponent(vnode, parent);
29
- }
30
- const el = document.createElement(vnode.tag);
31
- applyProps(el, vnode.props, {});
32
- for (const child of vnode.children) {
33
- const node = createDOM(child, el);
34
- if (node) el.appendChild(node);
35
- }
36
- el._vnode = vnode;
37
- return el;
38
- }
67
+ if (typeof container === 'string') {
68
+ container = document.querySelector(container);
69
+ }
70
+ disposeTree(container); // Clean up any previous mount
71
+ container.textContent = '';
72
+ const node = createDOM(vnode, container);
73
+ if (node) container.appendChild(node);
74
+ return () => {
75
+ disposeTree(container);
76
+ container.textContent = '';
77
+ };
78
+ }
79
+
80
+ // --- Create DOM from VNode ---
81
+
82
+ function createDOM(vnode, parent, isSvg) {
83
+ // Null/false/true placeholder comment (preserves child indices for reconciliation)
84
+ if (vnode == null || vnode === false || vnode === true) {
85
+ return document.createComment('');
86
+ }
87
+
88
+ // Text
89
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
90
+ return document.createTextNode(String(vnode));
91
+ }
92
+
93
+ // Array (fragment)
94
+ if (Array.isArray(vnode)) {
95
+ const frag = document.createDocumentFragment();
96
+ for (const child of vnode) {
97
+ const node = createDOM(child, parent, isSvg);
98
+ if (node) frag.appendChild(node);
99
+ }
100
+ return frag;
101
+ }
102
+
103
+ // Component
104
+ if (typeof vnode.tag === 'function') {
105
+ return createComponent(vnode, parent, isSvg);
106
+ }
107
+
108
+ // Detect SVG context: either we're already in SVG, or this tag is an SVG element
109
+ const svgContext = isSvg || vnode.tag === 'svg' || SVG_ELEMENTS.has(vnode.tag);
110
+
111
+ // HTML or SVG Element
112
+ const el = svgContext
113
+ ? document.createElementNS(SVG_NS, vnode.tag)
114
+ : document.createElement(vnode.tag);
115
+
116
+ applyProps(el, vnode.props, {}, svgContext);
117
+ for (const child of vnode.children) {
118
+ const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
119
+ if (node) el.appendChild(node);
120
+ }
121
+
122
+ // Store vnode on element for diffing
123
+ el._vnode = vnode;
124
+ return el;
125
+ }
126
+
127
+ // --- Component Rendering ---
128
+
39
129
  const componentStack = [];
130
+
40
131
  export function getCurrentComponent() {
41
- return componentStack[componentStack.length - 1];
42
- }
43
- function createComponent(vnode, parent) {
44
- const { tag: Component, props, children } = vnode;
45
- if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
46
- return createErrorBoundary(vnode, parent);
47
- }
48
- if (Component === '__suspense' || vnode.tag === '__suspense') {
49
- return createSuspenseBoundary(vnode, parent);
50
- }
51
- const ctx = {
52
- hooks: [],
53
- hookIndex: 0,
54
- effects: [],
55
- cleanups: [],
56
- mounted: false,
57
- disposed: false,
58
- };
59
- const marker = document.createComment(`w:${Component.name || 'anon'}`);
60
- let currentNodes = [];
61
- const dispose = effect(() => {
62
- if (ctx.disposed) return;
63
- ctx.hookIndex = 0;
64
- componentStack.push(ctx);
65
- let result;
66
- try {
67
- result = Component({ ...props, children });
68
- } catch (error) {
69
- componentStack.pop();
70
- if (!reportError(error)) {
71
- console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
72
- throw error;
73
- }
74
- return;
75
- }
76
- componentStack.pop();
77
- const vnodes = Array.isArray(result) ? result : [result];
78
- if (!ctx.mounted) {
79
- ctx.mounted = true;
80
- for (const v of vnodes) {
81
- const node = createDOM(v, parent);
82
- if (node) {
83
- currentNodes.push(node);
84
- }
85
- }
86
- } else {
87
- reconcile(marker.parentNode, currentNodes, vnodes, marker);
88
- }
89
- });
90
- ctx.effects.push(dispose);
91
- const frag = document.createDocumentFragment();
92
- frag.appendChild(marker);
93
- for (const node of currentNodes) {
94
- frag.appendChild(node);
95
- }
96
- return frag;
97
- }
132
+ return componentStack[componentStack.length - 1];
133
+ }
134
+
135
+ export function getComponentStack() {
136
+ return componentStack;
137
+ }
138
+
139
+ function createComponent(vnode, parent, isSvg) {
140
+ const { tag: Component, props, children } = vnode;
141
+
142
+ // Handle special boundary components
143
+ if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
144
+ return createErrorBoundary(vnode, parent);
145
+ }
146
+ if (Component === '__suspense' || vnode.tag === '__suspense') {
147
+ return createSuspenseBoundary(vnode, parent);
148
+ }
149
+
150
+ // Component context for hooks
151
+ const ctx = {
152
+ hooks: [],
153
+ hookIndex: 0,
154
+ effects: [],
155
+ cleanups: [],
156
+ mounted: false,
157
+ disposed: false,
158
+ Component, // Store for identity check in patchNode
159
+ };
160
+
161
+ // Wrapper element: <what-c display:contents> for HTML, <g> for SVG
162
+ let wrapper;
163
+ if (isSvg) {
164
+ wrapper = document.createElementNS(SVG_NS, 'g');
165
+ } else {
166
+ wrapper = document.createElement('what-c');
167
+ wrapper.style.display = 'contents';
168
+ }
169
+ wrapper._componentCtx = ctx;
170
+ wrapper._isSvg = !!isSvg;
171
+ ctx._wrapper = wrapper;
172
+
173
+ // Track for disposal
174
+ mountedComponents.add(ctx);
175
+
176
+ // Props signal for reactive updates from parent
177
+ const propsSignal = signal({ ...props, children });
178
+ ctx._propsSignal = propsSignal;
179
+
180
+ // Reactive render: re-renders when signals used inside change
181
+ const dispose = effect(() => {
182
+ if (ctx.disposed) return;
183
+ ctx.hookIndex = 0;
184
+
185
+ componentStack.push(ctx);
186
+
187
+ let result;
188
+ try {
189
+ result = Component(propsSignal());
190
+ } catch (error) {
191
+ componentStack.pop();
192
+ if (!reportError(error)) {
193
+ console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
194
+ throw error;
195
+ }
196
+ return;
197
+ }
198
+
199
+ componentStack.pop();
200
+
201
+ const vnodes = Array.isArray(result) ? result : [result];
202
+
203
+ if (!ctx.mounted) {
204
+ // Initial mount
205
+ ctx.mounted = true;
206
+
207
+ // Run onMount callbacks after DOM is ready
208
+ if (ctx._mountCallbacks) {
209
+ queueMicrotask(() => {
210
+ if (ctx.disposed) return;
211
+ for (const fn of ctx._mountCallbacks) {
212
+ try { fn(); } catch (e) { console.error('[what] onMount error:', e); }
213
+ }
214
+ });
215
+ }
216
+
217
+ for (const v of vnodes) {
218
+ const node = createDOM(v, wrapper, isSvg);
219
+ if (node) wrapper.appendChild(node);
220
+ }
221
+ } else {
222
+ // Update: reconcile children inside wrapper
223
+ reconcileChildren(wrapper, vnodes);
224
+ }
225
+ });
226
+
227
+ ctx.effects.push(dispose);
228
+ return wrapper;
229
+ }
230
+
231
+ // Error boundary component handler
98
232
  function createErrorBoundary(vnode, parent) {
99
- const { errorState, handleError, fallback, reset } = vnode.props;
100
- const children = vnode.children;
101
- const marker = document.createComment('w:errorBoundary');
102
- let currentNodes = [];
103
- const boundary = { handleError };
104
- const dispose = effect(() => {
105
- const error = errorState();
106
- errorBoundaryStack.push(boundary);
107
- let vnodes;
108
- if (error) {
109
- if (typeof fallback === 'function') {
110
- vnodes = [fallback({ error, reset })];
111
- } else {
112
- vnodes = [fallback];
113
- }
114
- } else {
115
- vnodes = children;
116
- }
117
- errorBoundaryStack.pop();
118
- vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
119
- if (currentNodes.length === 0) {
120
- for (const v of vnodes) {
121
- const node = createDOM(v, parent);
122
- if (node) currentNodes.push(node);
123
- }
124
- } else {
125
- reconcile(marker.parentNode, currentNodes, vnodes, marker);
126
- }
127
- });
128
- const frag = document.createDocumentFragment();
129
- frag.appendChild(marker);
130
- for (const node of currentNodes) {
131
- frag.appendChild(node);
132
- }
133
- return frag;
134
- }
233
+ const { errorState, handleError, fallback, reset } = vnode.props;
234
+ const children = vnode.children;
235
+
236
+ const wrapper = document.createElement('what-c');
237
+ wrapper.style.display = 'contents';
238
+
239
+ const dispose = effect(() => {
240
+ const error = errorState();
241
+
242
+ errorBoundaryStack.push({ handleError });
243
+
244
+ let vnodes;
245
+ if (error) {
246
+ vnodes = typeof fallback === 'function' ? [fallback({ error, reset })] : [fallback];
247
+ } else {
248
+ vnodes = children;
249
+ }
250
+
251
+ errorBoundaryStack.pop();
252
+ vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
253
+
254
+ if (wrapper.childNodes.length === 0) {
255
+ for (const v of vnodes) {
256
+ const node = createDOM(v, wrapper);
257
+ if (node) wrapper.appendChild(node);
258
+ }
259
+ } else {
260
+ reconcileChildren(wrapper, vnodes);
261
+ }
262
+ });
263
+
264
+ return wrapper;
265
+ }
266
+
267
+ // Suspense boundary component handler
135
268
  function createSuspenseBoundary(vnode, parent) {
136
- const { boundary, fallback, loading } = vnode.props;
137
- const children = vnode.children;
138
- const marker = document.createComment('w:suspense');
139
- let currentNodes = [];
140
- const dispose = effect(() => {
141
- const isLoading = loading();
142
- const vnodes = isLoading ? [fallback] : children;
143
- const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
144
- if (currentNodes.length === 0) {
145
- for (const v of normalized) {
146
- const node = createDOM(v, parent);
147
- if (node) currentNodes.push(node);
148
- }
149
- } else {
150
- reconcile(marker.parentNode, currentNodes, normalized, marker);
151
- }
152
- });
153
- const frag = document.createDocumentFragment();
154
- frag.appendChild(marker);
155
- for (const node of currentNodes) {
156
- frag.appendChild(node);
157
- }
158
- return frag;
159
- }
269
+ const { boundary, fallback, loading } = vnode.props;
270
+ const children = vnode.children;
271
+
272
+ const wrapper = document.createElement('what-c');
273
+ wrapper.style.display = 'contents';
274
+
275
+ const dispose = effect(() => {
276
+ const isLoading = loading();
277
+ const vnodes = isLoading ? [fallback] : children;
278
+ const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
279
+
280
+ if (wrapper.childNodes.length === 0) {
281
+ for (const v of normalized) {
282
+ const node = createDOM(v, wrapper);
283
+ if (node) wrapper.appendChild(node);
284
+ }
285
+ } else {
286
+ reconcileChildren(wrapper, normalized);
287
+ }
288
+ });
289
+
290
+ return wrapper;
291
+ }
292
+
293
+ // --- Reconciliation ---
294
+ // Diff old DOM nodes against new VNodes, patch in place.
295
+ // Uses keyed reconciliation with LIS (Longest Increasing Subsequence) for minimal DOM moves.
296
+
160
297
  function reconcile(parent, oldNodes, newVNodes, beforeMarker) {
161
- if (!parent) return;
162
- const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
163
- if (hasKeys) {
164
- reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
165
- } else {
166
- reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
167
- }
168
- }
298
+ if (!parent) return;
299
+
300
+ const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
301
+
302
+ if (hasKeys) {
303
+ reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
304
+ } else {
305
+ reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
306
+ }
307
+ }
308
+
309
+ // Unkeyed reconciliation (index-based, fast for static lists)
169
310
  function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
170
- const maxLen = Math.max(oldNodes.length, newVNodes.length);
171
- const newNodes = [];
172
- for (let i = 0; i < maxLen; i++) {
173
- const oldNode = oldNodes[i];
174
- const newVNode = newVNodes[i];
175
- if (i >= newVNodes.length) {
176
- if (oldNode && oldNode.parentNode) {
177
- oldNode.parentNode.removeChild(oldNode);
178
- }
179
- continue;
180
- }
181
- if (i >= oldNodes.length) {
182
- const node = createDOM(newVNode, parent);
183
- if (node) {
184
- const ref = getInsertionRef(oldNodes, beforeMarker);
185
- parent.insertBefore(node, ref);
186
- newNodes.push(node);
187
- }
188
- continue;
189
- }
190
- const patched = patchNode(parent, oldNode, newVNode);
191
- newNodes.push(patched);
192
- }
193
- oldNodes.length = 0;
194
- oldNodes.push(...newNodes);
195
- }
311
+ const maxLen = Math.max(oldNodes.length, newVNodes.length);
312
+ const newNodes = [];
313
+
314
+ for (let i = 0; i < maxLen; i++) {
315
+ const oldNode = oldNodes[i];
316
+ const newVNode = newVNodes[i];
317
+
318
+ if (i >= newVNodes.length) {
319
+ // Remove extra old nodes
320
+ if (oldNode && oldNode.parentNode) {
321
+ disposeTree(oldNode);
322
+ oldNode.parentNode.removeChild(oldNode);
323
+ }
324
+ continue;
325
+ }
326
+
327
+ if (i >= oldNodes.length) {
328
+ // Append new nodes
329
+ const node = createDOM(newVNode, parent);
330
+ if (node) {
331
+ const ref = getInsertionRef(oldNodes, beforeMarker);
332
+ parent.insertBefore(node, ref);
333
+ newNodes.push(node);
334
+ }
335
+ continue;
336
+ }
337
+
338
+ // Patch existing node
339
+ const patched = patchNode(parent, oldNode, newVNode);
340
+ newNodes.push(patched);
341
+ }
342
+
343
+ // Update the reference array
344
+ oldNodes.length = 0;
345
+ oldNodes.push(...newNodes);
346
+ }
347
+
348
+ // Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
196
349
  function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
197
- const oldKeyMap = new Map();
198
- for (let i = 0; i < oldNodes.length; i++) {
199
- const node = oldNodes[i];
200
- const key = node._vnode?.key;
201
- if (key != null) {
202
- oldKeyMap.set(key, { node, index: i });
203
- }
204
- }
205
- const newNodes = [];
206
- const newLen = newVNodes.length;
207
- const sources = new Array(newLen).fill(-1);
208
- const reused = new Set();
209
- for (let i = 0; i < newLen; i++) {
210
- const vnode = newVNodes[i];
211
- const key = vnode?.key;
212
- if (key != null && oldKeyMap.has(key)) {
213
- const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
214
- sources[i] = oldIndex;
215
- reused.add(oldIndex);
216
- }
217
- }
218
- for (let i = 0; i < oldNodes.length; i++) {
219
- if (!reused.has(i) && oldNodes[i]?.parentNode) {
220
- oldNodes[i].parentNode.removeChild(oldNodes[i]);
221
- }
222
- }
223
- const lis = longestIncreasingSubsequence(sources.filter(s => s !== -1));
224
- const lisSet = new Set(lis.map((_, i) => {
225
- let count = 0;
226
- for (let j = 0; j < sources.length; j++) {
227
- if (sources[j] !== -1) {
228
- if (count === lis[i]) return j;
229
- count++;
230
- }
231
- }
232
- return -1;
233
- }));
234
- let lastInserted = beforeMarker?.nextSibling || null;
235
- for (let i = newLen - 1; i >= 0; i--) {
236
- const vnode = newVNodes[i];
237
- const key = vnode?.key;
238
- const oldEntry = key != null ? oldKeyMap.get(key) : null;
239
- if (oldEntry && sources[i] !== -1) {
240
- const oldNode = oldEntry.node;
241
- const patched = patchNode(parent, oldNode, vnode);
242
- newNodes[i] = patched;
243
- if (!lisSet.has(i) && patched.parentNode) {
244
- parent.insertBefore(patched, lastInserted);
245
- }
246
- lastInserted = patched;
247
- } else {
248
- const node = createDOM(vnode, parent);
249
- if (node) {
250
- parent.insertBefore(node, lastInserted);
251
- lastInserted = node;
252
- }
253
- newNodes[i] = node;
254
- }
255
- }
256
- oldNodes.length = 0;
257
- oldNodes.push(...newNodes.filter(Boolean));
258
- }
350
+ // Build old key -> { node, index } map
351
+ const oldKeyMap = new Map();
352
+ for (let i = 0; i < oldNodes.length; i++) {
353
+ const node = oldNodes[i];
354
+ const key = node._vnode?.key;
355
+ if (key != null) {
356
+ oldKeyMap.set(key, { node, index: i });
357
+ }
358
+ }
359
+
360
+ const newNodes = [];
361
+ const newLen = newVNodes.length;
362
+
363
+ // First pass: match keys and find reusable nodes
364
+ const sources = new Array(newLen).fill(-1); // Maps new index to old index
365
+ const reused = new Set();
366
+
367
+ for (let i = 0; i < newLen; i++) {
368
+ const vnode = newVNodes[i];
369
+ const key = vnode?.key;
370
+ if (key != null && oldKeyMap.has(key)) {
371
+ const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
372
+ sources[i] = oldIndex;
373
+ reused.add(oldIndex);
374
+ }
375
+ }
376
+
377
+ // Remove nodes that aren't reused
378
+ for (let i = 0; i < oldNodes.length; i++) {
379
+ if (!reused.has(i) && oldNodes[i]?.parentNode) {
380
+ disposeTree(oldNodes[i]);
381
+ oldNodes[i].parentNode.removeChild(oldNodes[i]);
382
+ }
383
+ }
384
+
385
+ // Find LIS of old indices to determine which nodes don't need to move
386
+ const lis = longestIncreasingSubsequence(sources.filter(s => s !== -1));
387
+ const lisSet = new Set(lis.map((_, i) => {
388
+ let count = 0;
389
+ for (let j = 0; j < sources.length; j++) {
390
+ if (sources[j] !== -1) {
391
+ if (count === lis[i]) return j;
392
+ count++;
393
+ }
394
+ }
395
+ return -1;
396
+ }));
397
+
398
+ // Build new nodes array and move/create as needed
399
+ let lastInserted = beforeMarker?.nextSibling || null;
400
+
401
+ // Process in reverse order for correct insertion
402
+ for (let i = newLen - 1; i >= 0; i--) {
403
+ const vnode = newVNodes[i];
404
+ const key = vnode?.key;
405
+ const oldEntry = key != null ? oldKeyMap.get(key) : null;
406
+
407
+ if (oldEntry && sources[i] !== -1) {
408
+ // Reuse existing node
409
+ const oldNode = oldEntry.node;
410
+ // Patch props/children
411
+ const patched = patchNode(parent, oldNode, vnode);
412
+ newNodes[i] = patched;
413
+
414
+ // Move if not in LIS
415
+ if (!lisSet.has(i) && patched.parentNode) {
416
+ parent.insertBefore(patched, lastInserted);
417
+ }
418
+ lastInserted = patched;
419
+ } else {
420
+ // Create new node
421
+ const node = createDOM(vnode, parent);
422
+ if (node) {
423
+ parent.insertBefore(node, lastInserted);
424
+ lastInserted = node;
425
+ }
426
+ newNodes[i] = node;
427
+ }
428
+ }
429
+
430
+ // Update the reference array
431
+ oldNodes.length = 0;
432
+ oldNodes.push(...newNodes.filter(Boolean));
433
+ }
434
+
435
+ // Longest Increasing Subsequence - O(n log n)
436
+ // Returns indices of elements that form the LIS
259
437
  function longestIncreasingSubsequence(arr) {
260
- if (arr.length === 0) return [];
261
- const n = arr.length;
262
- const dp = new Array(n).fill(1);
263
- const parent = new Array(n).fill(-1);
264
- const tails = [0];
265
- for (let i = 1; i < n; i++) {
266
- if (arr[i] > arr[tails[tails.length - 1]]) {
267
- parent[i] = tails[tails.length - 1];
268
- tails.push(i);
269
- } else {
270
- let lo = 0, hi = tails.length - 1;
271
- while (lo < hi) {
272
- const mid = (lo + hi) >> 1;
273
- if (arr[tails[mid]] < arr[i]) lo = mid + 1;
274
- else hi = mid;
275
- }
276
- if (arr[i] < arr[tails[lo]]) {
277
- if (lo > 0) parent[i] = tails[lo - 1];
278
- tails[lo] = i;
279
- }
280
- }
281
- }
282
- const result = [];
283
- let k = tails[tails.length - 1];
284
- while (k !== -1) {
285
- result.push(k);
286
- k = parent[k];
287
- }
288
- return result.reverse();
289
- }
438
+ if (arr.length === 0) return [];
439
+
440
+ const n = arr.length;
441
+ const dp = new Array(n).fill(1); // Length of LIS ending at i
442
+ const parent = new Array(n).fill(-1); // Parent index for reconstruction
443
+ const tails = [0]; // Indices of smallest tail elements
444
+
445
+ for (let i = 1; i < n; i++) {
446
+ if (arr[i] > arr[tails[tails.length - 1]]) {
447
+ parent[i] = tails[tails.length - 1];
448
+ tails.push(i);
449
+ } else {
450
+ // Binary search for the smallest element >= arr[i]
451
+ let lo = 0, hi = tails.length - 1;
452
+ while (lo < hi) {
453
+ const mid = (lo + hi) >> 1;
454
+ if (arr[tails[mid]] < arr[i]) lo = mid + 1;
455
+ else hi = mid;
456
+ }
457
+ if (arr[i] < arr[tails[lo]]) {
458
+ if (lo > 0) parent[i] = tails[lo - 1];
459
+ tails[lo] = i;
460
+ }
461
+ }
462
+ }
463
+
464
+ // Reconstruct LIS
465
+ const result = [];
466
+ let k = tails[tails.length - 1];
467
+ while (k !== -1) {
468
+ result.push(k);
469
+ k = parent[k];
470
+ }
471
+ return result.reverse();
472
+ }
473
+
290
474
  function getInsertionRef(nodes, marker) {
291
- if (nodes.length > 0) {
292
- const last = nodes[nodes.length - 1];
293
- return last.nextSibling;
294
- }
295
- return marker ? marker.nextSibling : null;
475
+ if (nodes.length > 0) {
476
+ const last = nodes[nodes.length - 1];
477
+ return last.nextSibling;
478
+ }
479
+ return marker ? marker.nextSibling : null;
296
480
  }
481
+
297
482
  function patchNode(parent, domNode, vnode) {
298
- if (vnode == null || vnode === false || vnode === true) {
299
- if (domNode && domNode.parentNode) domNode.parentNode.removeChild(domNode);
300
- return null;
301
- }
302
- if (typeof vnode === 'string' || typeof vnode === 'number') {
303
- const text = String(vnode);
304
- if (domNode.nodeType === 3) {
305
- if (domNode.textContent !== text) domNode.textContent = text;
306
- return domNode;
307
- }
308
- const newNode = document.createTextNode(text);
309
- parent.replaceChild(newNode, domNode);
310
- return newNode;
311
- }
312
- if (Array.isArray(vnode)) {
313
- const frag = document.createDocumentFragment();
314
- for (const v of vnode) {
315
- const node = createDOM(v, parent);
316
- if (node) frag.appendChild(node);
317
- }
318
- parent.replaceChild(frag, domNode);
319
- return frag;
320
- }
321
- if (typeof vnode.tag === 'function') {
322
- const node = createComponent(vnode, parent);
323
- parent.replaceChild(node, domNode);
324
- return node;
325
- }
326
- if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
327
- const oldProps = domNode._vnode?.props || {};
328
- applyProps(domNode, vnode.props, oldProps);
329
- reconcileChildren(domNode, vnode.children);
330
- domNode._vnode = vnode;
331
- return domNode;
332
- }
333
- const newNode = createDOM(vnode, parent);
334
- parent.replaceChild(newNode, domNode);
335
- return newNode;
336
- }
483
+ // Null/removed keep placeholder or replace with one
484
+ if (vnode == null || vnode === false || vnode === true) {
485
+ if (domNode && domNode.nodeType === 8 && !domNode._componentCtx) {
486
+ return domNode; // already a placeholder comment
487
+ }
488
+ const placeholder = document.createComment('');
489
+ if (domNode && domNode.parentNode) {
490
+ disposeTree(domNode);
491
+ parent.replaceChild(placeholder, domNode);
492
+ }
493
+ return placeholder;
494
+ }
495
+
496
+ // Text
497
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
498
+ const text = String(vnode);
499
+ if (domNode.nodeType === 3) {
500
+ if (domNode.textContent !== text) domNode.textContent = text;
501
+ return domNode;
502
+ }
503
+ const newNode = document.createTextNode(text);
504
+ disposeTree(domNode);
505
+ parent.replaceChild(newNode, domNode);
506
+ return newNode;
507
+ }
508
+
509
+ // Array
510
+ if (Array.isArray(vnode)) {
511
+ // For now, flatten and reconcile as children
512
+ const frag = document.createDocumentFragment();
513
+ for (const v of vnode) {
514
+ const node = createDOM(v, parent);
515
+ if (node) frag.appendChild(node);
516
+ }
517
+ disposeTree(domNode);
518
+ parent.replaceChild(frag, domNode);
519
+ return frag;
520
+ }
521
+
522
+ // Component
523
+ if (typeof vnode.tag === 'function') {
524
+ // Check if old node is a component wrapper for the same component
525
+ if (domNode._componentCtx && !domNode._componentCtx.disposed
526
+ && domNode._componentCtx.Component === vnode.tag) {
527
+ // Same component — update props reactively, let its effect re-render
528
+ domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
529
+ return domNode;
530
+ }
531
+ // Different component or not a component — dispose old, create new
532
+ disposeTree(domNode);
533
+ const node = createComponent(vnode, parent);
534
+ parent.replaceChild(node, domNode);
535
+ return node;
536
+ }
537
+
538
+ // Element: same tag? Patch props + children
539
+ if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
540
+ const oldProps = domNode._vnode?.props || {};
541
+ applyProps(domNode, vnode.props, oldProps);
542
+ reconcileChildren(domNode, vnode.children);
543
+ domNode._vnode = vnode;
544
+ return domNode;
545
+ }
546
+
547
+ // Different tag: replace entirely
548
+ const newNode = createDOM(vnode, parent);
549
+ disposeTree(domNode);
550
+ parent.replaceChild(newNode, domNode);
551
+ return newNode;
552
+ }
553
+
337
554
  function reconcileChildren(parent, newChildVNodes) {
338
- const oldChildren = Array.from(parent.childNodes);
339
- const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
340
- if (hasKeys) {
341
- reconcileKeyed(parent, oldChildren, newChildVNodes, null);
342
- } else {
343
- const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
344
- for (let i = 0; i < maxLen; i++) {
345
- if (i >= newChildVNodes.length) {
346
- if (oldChildren[i]?.parentNode) {
347
- parent.removeChild(oldChildren[i]);
348
- }
349
- continue;
350
- }
351
- if (i >= oldChildren.length) {
352
- const node = createDOM(newChildVNodes[i], parent);
353
- if (node) parent.appendChild(node);
354
- continue;
355
- }
356
- patchNode(parent, oldChildren[i], newChildVNodes[i]);
357
- }
358
- }
359
- }
360
- function applyProps(el, newProps, oldProps) {
361
- newProps = newProps || {};
362
- oldProps = oldProps || {};
363
- for (const key in oldProps) {
364
- if (key === 'key' || key === 'ref' || key === 'children') continue;
365
- if (!(key in newProps)) {
366
- removeProp(el, key, oldProps[key]);
367
- }
368
- }
369
- for (const key in newProps) {
370
- if (key === 'key' || key === 'ref' || key === 'children') continue;
371
- if (newProps[key] !== oldProps[key]) {
372
- setProp(el, key, newProps[key]);
373
- }
374
- }
375
- if (newProps.ref && newProps.ref !== oldProps.ref) {
376
- if (typeof newProps.ref === 'function') newProps.ref(el);
377
- else newProps.ref.current = el;
378
- }
379
- }
380
- function setProp(el, key, value) {
381
- if (key.startsWith('on') && key.length > 2) {
382
- const event = key.slice(2).toLowerCase();
383
- const old = el._events?.[event];
384
- if (old) el.removeEventListener(event, old);
385
- if (!el._events) el._events = {};
386
- const wrappedHandler = (e) => untrack(() => value(e));
387
- wrappedHandler._original = value;
388
- el._events[event] = wrappedHandler;
389
- el.addEventListener(event, wrappedHandler);
390
- return;
391
- }
392
- if (key === 'className' || key === 'class') {
393
- el.className = value || '';
394
- return;
395
- }
396
- if (key === 'style') {
397
- if (typeof value === 'string') {
398
- el.style.cssText = value;
399
- } else if (typeof value === 'object') {
400
- for (const prop in value) {
401
- el.style[prop] = value[prop] ?? '';
402
- }
403
- }
404
- return;
405
- }
406
- if (key === 'dangerouslySetInnerHTML') {
407
- el.innerHTML = value.__html;
408
- return;
409
- }
410
- if (typeof value === 'boolean') {
411
- if (value) el.setAttribute(key, '');
412
- else el.removeAttribute(key);
413
- return;
414
- }
415
- if (key.startsWith('data-') || key.startsWith('aria-')) {
416
- el.setAttribute(key, value);
417
- return;
418
- }
419
- if (key in el) {
420
- el[key] = value;
421
- } else {
422
- el.setAttribute(key, value);
423
- }
424
- }
555
+ const oldChildren = Array.from(parent.childNodes);
556
+
557
+ // Check for keyed children
558
+ const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
559
+
560
+ if (hasKeys) {
561
+ // Use keyed reconciliation
562
+ reconcileKeyed(parent, oldChildren, newChildVNodes, null);
563
+ } else {
564
+ // Unkeyed reconciliation
565
+ const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
566
+
567
+ for (let i = 0; i < maxLen; i++) {
568
+ if (i >= newChildVNodes.length) {
569
+ // Remove extra
570
+ if (oldChildren[i]?.parentNode) {
571
+ disposeTree(oldChildren[i]);
572
+ parent.removeChild(oldChildren[i]);
573
+ }
574
+ continue;
575
+ }
576
+
577
+ if (i >= oldChildren.length) {
578
+ // Append new
579
+ const node = createDOM(newChildVNodes[i], parent);
580
+ if (node) parent.appendChild(node);
581
+ continue;
582
+ }
583
+
584
+ patchNode(parent, oldChildren[i], newChildVNodes[i]);
585
+ }
586
+ }
587
+ }
588
+
589
+ // --- Prop Diffing ---
590
+ // Only touch DOM for props that actually changed.
591
+
592
+ function applyProps(el, newProps, oldProps, isSvg) {
593
+ newProps = newProps || {};
594
+ oldProps = oldProps || {};
595
+
596
+ // Remove old props not in new
597
+ for (const key in oldProps) {
598
+ if (key === 'key' || key === 'ref' || key === 'children') continue;
599
+ if (!(key in newProps)) {
600
+ removeProp(el, key, oldProps[key]);
601
+ }
602
+ }
603
+
604
+ // Set new/changed props
605
+ for (const key in newProps) {
606
+ if (key === 'key' || key === 'ref' || key === 'children') continue;
607
+ if (newProps[key] !== oldProps[key]) {
608
+ setProp(el, key, newProps[key], isSvg);
609
+ }
610
+ }
611
+
612
+ // Handle ref
613
+ if (newProps.ref && newProps.ref !== oldProps.ref) {
614
+ if (typeof newProps.ref === 'function') newProps.ref(el);
615
+ else newProps.ref.current = el;
616
+ }
617
+ }
618
+
619
+ function setProp(el, key, value, isSvg) {
620
+ // Event handlers: onClick -> click
621
+ // Wrap in untrack so signal reads in handlers don't create subscriptions
622
+ if (key.startsWith('on') && key.length > 2) {
623
+ const event = key.slice(2).toLowerCase();
624
+ // Store handler for removal
625
+ const old = el._events?.[event];
626
+ if (old) el.removeEventListener(event, old);
627
+ if (!el._events) el._events = {};
628
+ // Wrap handler to untrack signal reads
629
+ const wrappedHandler = (e) => untrack(() => value(e));
630
+ wrappedHandler._original = value;
631
+ el._events[event] = wrappedHandler;
632
+ // Check for _eventOpts (once/capture/passive from compiler)
633
+ const eventOpts = value._eventOpts;
634
+ el.addEventListener(event, wrappedHandler, eventOpts || undefined);
635
+ return;
636
+ }
637
+
638
+ // className / class
639
+ if (key === 'className' || key === 'class') {
640
+ if (isSvg) {
641
+ el.setAttribute('class', value || '');
642
+ } else {
643
+ el.className = value || '';
644
+ }
645
+ return;
646
+ }
647
+
648
+ // Style object
649
+ if (key === 'style') {
650
+ if (typeof value === 'string') {
651
+ el.style.cssText = value;
652
+ } else if (typeof value === 'object') {
653
+ for (const prop in value) {
654
+ el.style[prop] = value[prop] ?? '';
655
+ }
656
+ }
657
+ return;
658
+ }
659
+
660
+ // dangerouslySetInnerHTML
661
+ if (key === 'dangerouslySetInnerHTML') {
662
+ el.innerHTML = value.__html;
663
+ return;
664
+ }
665
+
666
+ // Boolean attributes
667
+ if (typeof value === 'boolean') {
668
+ if (value) el.setAttribute(key, '');
669
+ else el.removeAttribute(key);
670
+ return;
671
+ }
672
+
673
+ // data-* and aria-* as attributes
674
+ if (key.startsWith('data-') || key.startsWith('aria-')) {
675
+ el.setAttribute(key, value);
676
+ return;
677
+ }
678
+
679
+ // SVG: always use setAttribute (SVG properties don't work as DOM properties)
680
+ if (isSvg) {
681
+ if (value === false || value == null) {
682
+ el.removeAttribute(key);
683
+ } else {
684
+ el.setAttribute(key, value === true ? '' : String(value));
685
+ }
686
+ return;
687
+ }
688
+
689
+ // Default: set as property if it exists, otherwise attribute
690
+ if (key in el) {
691
+ el[key] = value;
692
+ } else {
693
+ el.setAttribute(key, value);
694
+ }
695
+ }
696
+
425
697
  function removeProp(el, key, oldValue) {
426
- if (key.startsWith('on') && key.length > 2) {
427
- const event = key.slice(2).toLowerCase();
428
- if (el._events?.[event]) {
429
- el.removeEventListener(event, el._events[event]);
430
- delete el._events[event];
431
- }
432
- return;
433
- }
434
- if (key === 'className' || key === 'class') {
435
- el.className = '';
436
- return;
437
- }
438
- if (key === 'style') {
439
- el.style.cssText = '';
440
- return;
698
+ if (key.startsWith('on') && key.length > 2) {
699
+ const event = key.slice(2).toLowerCase();
700
+ if (el._events?.[event]) {
701
+ el.removeEventListener(event, el._events[event]);
702
+ delete el._events[event];
703
+ }
704
+ return;
705
+ }
706
+
707
+ if (key === 'className' || key === 'class') {
708
+ el.className = '';
709
+ return;
710
+ }
711
+
712
+ if (key === 'style') {
713
+ el.style.cssText = '';
714
+ return;
715
+ }
716
+
717
+ el.removeAttribute(key);
441
718
  }
442
- el.removeAttribute(key);
443
- }