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