@ccheever/exact-renderer 0.1.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.
Files changed (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
@@ -0,0 +1,1736 @@
1
+ /**
2
+ * Exact DOM Shim
3
+ *
4
+ * RFC 0043 introduces a compatibility layer for DOM-targeting frameworks such
5
+ * as Svelte. The shim exposes a small DOM-like surface that translates tree
6
+ * mutations, prop writes, and event listeners into Exact's shared host-ops.
7
+ *
8
+ * This is intentionally not a full DOM implementation. It implements the
9
+ * subset of APIs that imperative UI runtimes typically use while rendering:
10
+ * - element/text/comment/document/fragment creation
11
+ * - append/insert/remove/reparent operations
12
+ * - attribute, classList, and style writes
13
+ * - event listener registration via EventTarget
14
+ * - per-root lifecycle (`destroy` / `reset`)
15
+ */
16
+
17
+ import {
18
+ getWindowViewportForRoot,
19
+ subscribeToRootWindowState,
20
+ type Unsubscribe,
21
+ } from '@exact/core';
22
+ import {
23
+ Event,
24
+ EventTarget,
25
+ FocusEvent,
26
+ KeyboardEvent,
27
+ type AddEventListenerOptions,
28
+ type EventListenerOptions,
29
+ type EventListenerOrEventListenerObject,
30
+ } from '@exact/runtime-js';
31
+
32
+ import {
33
+ NodeKind,
34
+ appendChild as hostAppendChild,
35
+ commitBatch,
36
+ createInstance,
37
+ createRoot,
38
+ createTextInstance,
39
+ defaultTagConfig,
40
+ destroyRoot,
41
+ detachChild as hostDetachChild,
42
+ getTagConfig,
43
+ insertBefore as hostInsertBefore,
44
+ nodeAppendChild as hostTreeAppendChild,
45
+ nodeInsertBefore as hostTreeInsertBefore,
46
+ nodeRemoveChild as hostTreeRemoveChild,
47
+ processEventProps,
48
+ removeChild as hostRemoveChild,
49
+ syncRootWindowState,
50
+ updateInstanceProps,
51
+ updateTextContent,
52
+ type ElementNode,
53
+ type RootNode,
54
+ type TextNode,
55
+ } from './host-ops.js';
56
+ import { DirtyFlags } from './nodes/node.js';
57
+
58
+ const __DEV__ = process.env.NODE_ENV !== 'production';
59
+
60
+ const PRESS_EVENT_TYPES = new Set(['click', 'pointerdown', 'pointerup']);
61
+ const CANCELABLE_EVENTS = new Set(['click', 'pointerdown', 'pointerup', 'keydown', 'keyup']);
62
+ const TAG_DEFAULTS: Record<string, Record<string, unknown>> = {
63
+ textarea: { multiline: true },
64
+ };
65
+ const ATTR_TO_PROP: Record<string, Record<string, string>> = {
66
+ textarea: { rows: 'numberOfLines' },
67
+ };
68
+
69
+ type ExactRenderableNode = ExactElement | ExactText;
70
+ type ExactChildNode = ExactElement | ExactText | ExactComment;
71
+ type ExactInsertableNode = ExactChildNode | ExactDocumentFragment;
72
+ type ExactParentNode = ExactElement | ExactRootBodyElement | ExactDocumentFragment;
73
+ type ExactAppendableNode = ExactInsertableNode | string;
74
+
75
+ function kebabToCamel(value: string): string {
76
+ return value.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
77
+ }
78
+
79
+ function camelToKebab(value: string): string {
80
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
81
+ }
82
+
83
+ function normalizeStylePropertyName(value: string): string {
84
+ return value.includes('-') ? kebabToCamel(value) : value;
85
+ }
86
+
87
+ function parseCssText(input: string): Record<string, unknown> {
88
+ const bag: Record<string, unknown> = {};
89
+
90
+ for (const segment of input.split(';')) {
91
+ const trimmed = segment.trim();
92
+ if (!trimmed) {
93
+ continue;
94
+ }
95
+
96
+ const separatorIndex = trimmed.indexOf(':');
97
+ if (separatorIndex === -1) {
98
+ continue;
99
+ }
100
+
101
+ const propertyName = normalizeStylePropertyName(
102
+ trimmed.slice(0, separatorIndex).trim(),
103
+ );
104
+ const propertyValue = trimmed.slice(separatorIndex + 1).trim();
105
+ if (!propertyName) {
106
+ continue;
107
+ }
108
+
109
+ bag[propertyName] = propertyValue;
110
+ }
111
+
112
+ return bag;
113
+ }
114
+
115
+ function cloneProps(props: Record<string, unknown>): Record<string, unknown> {
116
+ return {
117
+ ...props,
118
+ style:
119
+ props.style && typeof props.style === 'object'
120
+ ? { ...(props.style as Record<string, unknown>) }
121
+ : props.style,
122
+ };
123
+ }
124
+
125
+ function isRenderableNode(node: ExactInsertableNode): node is ExactRenderableNode {
126
+ return node instanceof ExactElement || node instanceof ExactText;
127
+ }
128
+
129
+ function isElementNode(node: ExactChildNode): node is ExactElement {
130
+ return node instanceof ExactElement;
131
+ }
132
+
133
+ function isCommentNode(node: ExactInsertableNode): node is ExactComment {
134
+ return node instanceof ExactComment;
135
+ }
136
+
137
+ function isCreatedNode(node: ElementNode | TextNode): boolean {
138
+ return (node.dirtyFlags & DirtyFlags.Created) !== 0;
139
+ }
140
+
141
+ function readViewport(rootId: number): { width: number; height: number } {
142
+ const snapshot = getWindowViewportForRoot(rootId);
143
+ return {
144
+ width: snapshot.width,
145
+ height: snapshot.height,
146
+ };
147
+ }
148
+
149
+ export abstract class ExactNodeBase extends EventTarget {
150
+ readonly ownerDocument: ExactDocument;
151
+ readonly __state: ExactDOMState;
152
+ readonly __rootId: number;
153
+ __parent: ExactParentNode | ExactDocumentElement | null = null;
154
+
155
+ constructor(state: ExactDOMState, ownerDocument: ExactDocument) {
156
+ super();
157
+ this.__state = state;
158
+ this.ownerDocument = ownerDocument;
159
+ this.__rootId = state.rootId;
160
+ }
161
+
162
+ abstract readonly nodeType: number;
163
+ abstract readonly nodeName: string;
164
+
165
+ get parentNode(): ExactParentNode | ExactDocumentElement | null {
166
+ return this.__parent;
167
+ }
168
+
169
+ get childNodes(): ExactChildNode[] {
170
+ return [];
171
+ }
172
+
173
+ get firstChild(): ExactChildNode | null {
174
+ return this.childNodes[0] ?? null;
175
+ }
176
+
177
+ get lastChild(): ExactChildNode | null {
178
+ const { childNodes } = this;
179
+ return childNodes[childNodes.length - 1] ?? null;
180
+ }
181
+
182
+ get nextSibling(): ExactChildNode | null {
183
+ const parent = this.__parent;
184
+ if (!parent || !(parent instanceof ExactContainerNodeBase)) {
185
+ return null;
186
+ }
187
+
188
+ const index = parent.__children.indexOf(this as unknown as ExactChildNode);
189
+ return index >= 0 ? parent.__children[index + 1] ?? null : null;
190
+ }
191
+
192
+ get previousSibling(): ExactChildNode | null {
193
+ const parent = this.__parent;
194
+ if (!parent || !(parent instanceof ExactContainerNodeBase)) {
195
+ return null;
196
+ }
197
+
198
+ const index = parent.__children.indexOf(this as unknown as ExactChildNode);
199
+ return index > 0 ? parent.__children[index - 1] ?? null : null;
200
+ }
201
+
202
+ remove(): void {
203
+ const parent = this.__parent;
204
+ if (parent instanceof ExactContainerNodeBase) {
205
+ parent.removeChild(this as unknown as ExactChildNode);
206
+ }
207
+ }
208
+
209
+ before(...nodes: ExactAppendableNode[]): void {
210
+ const parent = this.__parent;
211
+ if (!(parent instanceof ExactContainerNodeBase)) {
212
+ return;
213
+ }
214
+
215
+ for (const node of nodes) {
216
+ parent.insertBefore(
217
+ normalizeAppendableNode(this.ownerDocument, node),
218
+ this as unknown as ExactChildNode,
219
+ );
220
+ }
221
+ }
222
+
223
+ after(...nodes: ExactAppendableNode[]): void {
224
+ const parent = this.__parent;
225
+ if (!(parent instanceof ExactContainerNodeBase)) {
226
+ return;
227
+ }
228
+
229
+ let anchor = this.nextSibling;
230
+ for (const node of nodes) {
231
+ const normalized = normalizeAppendableNode(this.ownerDocument, node);
232
+ parent.insertBefore(normalized, anchor);
233
+ }
234
+ }
235
+
236
+ getRootNode(): ExactDocument {
237
+ return this.ownerDocument;
238
+ }
239
+
240
+ get isConnected(): boolean {
241
+ let current: ExactNodeBase | ExactDocumentElement | null = this;
242
+
243
+ while (current) {
244
+ if (current instanceof ExactRootBodyElement) {
245
+ return !current.__state.destroyed;
246
+ }
247
+ current = current.parentNode;
248
+ }
249
+
250
+ return false;
251
+ }
252
+
253
+ cloneNode(_deep: boolean = false): ExactNodeBase {
254
+ throw new Error('[ExactDOM] cloneNode() is not implemented for this node type.');
255
+ }
256
+
257
+ get textContent(): string {
258
+ return '';
259
+ }
260
+
261
+ set textContent(_value: string) {
262
+ // Overridden by renderable/text container subclasses.
263
+ }
264
+ }
265
+
266
+ export abstract class ExactContainerNodeBase extends ExactNodeBase {
267
+ __children: ExactChildNode[] = [];
268
+
269
+ append(...nodes: ExactAppendableNode[]): void {
270
+ for (const node of nodes) {
271
+ this.appendChild(normalizeAppendableNode(this.ownerDocument, node));
272
+ }
273
+ }
274
+
275
+ appendChild<T extends ExactInsertableNode>(child: T): T {
276
+ return insertNodeIntoParent(this as unknown as ExactParentNode, child, null) as T;
277
+ }
278
+
279
+ insertBefore<T extends ExactInsertableNode>(
280
+ child: T,
281
+ beforeChild: ExactChildNode | null,
282
+ ): T {
283
+ return insertNodeIntoParent(this as unknown as ExactParentNode, child, beforeChild) as T;
284
+ }
285
+
286
+ removeChild<T extends ExactChildNode>(child: T): T {
287
+ return removeNodeFromParent(this as unknown as ExactParentNode, child) as T;
288
+ }
289
+
290
+ get childNodes(): ExactChildNode[] {
291
+ return this.__children.slice();
292
+ }
293
+
294
+ get children(): ExactElement[] {
295
+ return this.__children.filter(isElementNode);
296
+ }
297
+
298
+ get firstChild(): ExactChildNode | null {
299
+ return this.__children[0] ?? null;
300
+ }
301
+
302
+ get lastChild(): ExactChildNode | null {
303
+ return this.__children[this.__children.length - 1] ?? null;
304
+ }
305
+
306
+ get textContent(): string {
307
+ return this.__children.map((child) => child.textContent).join('');
308
+ }
309
+
310
+ set textContent(value: string) {
311
+ for (const child of this.__children.slice()) {
312
+ this.removeChild(child);
313
+ }
314
+
315
+ if (value.length > 0) {
316
+ this.appendChild(this.ownerDocument.createTextNode(value));
317
+ }
318
+ }
319
+
320
+ abstract __getHostParent(): ElementNode | RootNode | null;
321
+
322
+ querySelector(selector: string): ExactElement | null {
323
+ return querySelectorInChildren(this.__children, selector);
324
+ }
325
+ }
326
+
327
+ class ExactClassList {
328
+ readonly __element: ExactElement;
329
+ readonly __tokens = new Set<string>();
330
+
331
+ constructor(element: ExactElement, initialClassName: string) {
332
+ this.__element = element;
333
+ this.__replaceFromString(initialClassName);
334
+ }
335
+
336
+ add(...tokens: string[]): void {
337
+ for (const token of tokens) {
338
+ if (token) {
339
+ this.__tokens.add(token);
340
+ }
341
+ }
342
+ this.__syncToElement();
343
+ }
344
+
345
+ remove(...tokens: string[]): void {
346
+ for (const token of tokens) {
347
+ this.__tokens.delete(token);
348
+ }
349
+ this.__syncToElement();
350
+ }
351
+
352
+ toggle(token: string, force?: boolean): boolean {
353
+ if (force === true) {
354
+ this.__tokens.add(token);
355
+ this.__syncToElement();
356
+ return true;
357
+ }
358
+ if (force === false) {
359
+ this.__tokens.delete(token);
360
+ this.__syncToElement();
361
+ return false;
362
+ }
363
+
364
+ if (this.__tokens.has(token)) {
365
+ this.__tokens.delete(token);
366
+ this.__syncToElement();
367
+ return false;
368
+ }
369
+
370
+ this.__tokens.add(token);
371
+ this.__syncToElement();
372
+ return true;
373
+ }
374
+
375
+ contains(token: string): boolean {
376
+ return this.__tokens.has(token);
377
+ }
378
+
379
+ toString(): string {
380
+ return Array.from(this.__tokens).join(' ');
381
+ }
382
+
383
+ __replaceFromString(className: string): void {
384
+ this.__tokens.clear();
385
+ for (const token of className.split(/\s+/)) {
386
+ if (token) {
387
+ this.__tokens.add(token);
388
+ }
389
+ }
390
+ }
391
+
392
+ __syncToElement(): void {
393
+ const className = this.toString();
394
+ if (className.length > 0) {
395
+ this.__element.__props.className = className;
396
+ } else {
397
+ delete this.__element.__props.className;
398
+ }
399
+ this.__element.__syncOriginalPropsIfNeeded();
400
+ this.__element.__markDirty();
401
+ }
402
+ }
403
+
404
+ class ExactStyleDeclarationCore {
405
+ readonly __element: ExactElement;
406
+ __bag: Record<string, unknown>;
407
+
408
+ constructor(element: ExactElement, initialStyle: Record<string, unknown> | undefined) {
409
+ this.__element = element;
410
+ this.__bag = initialStyle ? { ...initialStyle } : {};
411
+ }
412
+
413
+ get cssText(): string {
414
+ return Object.entries(this.__bag)
415
+ .map(([key, value]) => `${camelToKebab(key)}: ${String(value)}`)
416
+ .join('; ');
417
+ }
418
+
419
+ set cssText(value: string) {
420
+ this.__bag = parseCssText(value);
421
+ this.__syncToElement();
422
+ }
423
+
424
+ setProperty(name: string, value: string): void {
425
+ this.__set(normalizeStylePropertyName(name), value);
426
+ }
427
+
428
+ removeProperty(name: string): string {
429
+ const normalized = normalizeStylePropertyName(name);
430
+ const previous = this.__bag[normalized];
431
+ delete this.__bag[normalized];
432
+ this.__syncToElement();
433
+ return previous == null ? '' : String(previous);
434
+ }
435
+
436
+ getPropertyValue(name: string): string {
437
+ const normalized = normalizeStylePropertyName(name);
438
+ const value = this.__bag[normalized];
439
+ return value == null ? '' : String(value);
440
+ }
441
+
442
+ __set(name: string, value: unknown): void {
443
+ if (value === '' || value === null || value === undefined) {
444
+ delete this.__bag[name];
445
+ } else {
446
+ this.__bag[name] = value;
447
+ }
448
+ this.__syncToElement();
449
+ }
450
+
451
+ __snapshot(): Record<string, unknown> {
452
+ return { ...this.__bag };
453
+ }
454
+
455
+ __syncToElement(): void {
456
+ this.__element.__props.style = this.__snapshot();
457
+ this.__element.__syncOriginalPropsIfNeeded();
458
+ this.__element.__markDirty();
459
+ }
460
+ }
461
+
462
+ function createStyleDeclaration(
463
+ element: ExactElement,
464
+ initialStyle: Record<string, unknown> | undefined,
465
+ ): ExactStyleDeclarationCore {
466
+ const core = new ExactStyleDeclarationCore(element, initialStyle);
467
+
468
+ return new Proxy(core, {
469
+ get(target, prop, receiver) {
470
+ if (typeof prop === 'string') {
471
+ if (prop in target) {
472
+ return Reflect.get(target, prop, receiver);
473
+ }
474
+ return target.getPropertyValue(prop);
475
+ }
476
+ return Reflect.get(target, prop, receiver);
477
+ },
478
+ set(target, prop, value, receiver) {
479
+ if (typeof prop === 'string' && !(prop in target)) {
480
+ target.__set(normalizeStylePropertyName(prop), value);
481
+ return true;
482
+ }
483
+ return Reflect.set(target, prop, value, receiver);
484
+ },
485
+ });
486
+ }
487
+
488
+ export class ExactElement extends ExactContainerNodeBase {
489
+ readonly nodeType = 1;
490
+ readonly namespaceURI: string | null;
491
+ readonly tagName: string;
492
+ readonly nodeName: string;
493
+ __tagNameLower: string;
494
+ __node: ElementNode;
495
+ __tagConfig = defaultTagConfig;
496
+ __props: Record<string, unknown>;
497
+ __lastCommittedProps: Record<string, unknown>;
498
+ readonly classList: ExactClassList;
499
+ readonly style: ExactStyleDeclarationCore;
500
+ readonly __eventTypes = new Set<string>();
501
+ readonly __eventPropNames = new Map<string, string>();
502
+ readonly __eventCallbacks = new Map<string, Function>();
503
+
504
+ constructor(
505
+ state: ExactDOMState,
506
+ ownerDocument: ExactDocument,
507
+ tagName: string,
508
+ initialProps: Record<string, unknown>,
509
+ namespaceURI: string | null = null,
510
+ ) {
511
+ super(state, ownerDocument);
512
+ this.namespaceURI = namespaceURI;
513
+ this.__tagNameLower = tagName.toLowerCase();
514
+ this.tagName = this.__tagNameLower.toUpperCase();
515
+ this.nodeName = this.tagName;
516
+ this.__node = createInstance(tagName, initialProps);
517
+ this.__tagConfig = getTagConfig(tagName) ?? defaultTagConfig;
518
+ this.__props = cloneProps(initialProps);
519
+ this.__lastCommittedProps = cloneProps(initialProps);
520
+ this.classList = new ExactClassList(
521
+ this,
522
+ typeof this.__props.className === 'string' ? this.__props.className : '',
523
+ );
524
+ this.style = createStyleDeclaration(
525
+ this,
526
+ this.__props.style as Record<string, unknown> | undefined,
527
+ );
528
+
529
+ state.__registerWrapper(this.__node.id, this);
530
+ }
531
+
532
+ __getHostParent(): ElementNode | RootNode | null {
533
+ return this.__node;
534
+ }
535
+
536
+ get childNodes(): ExactChildNode[] {
537
+ return super.childNodes;
538
+ }
539
+
540
+ get firstChild(): ExactChildNode | null {
541
+ return super.firstChild;
542
+ }
543
+
544
+ get lastChild(): ExactChildNode | null {
545
+ return super.lastChild;
546
+ }
547
+
548
+ get className(): string {
549
+ return typeof this.__props.className === 'string' ? this.__props.className : '';
550
+ }
551
+
552
+ set className(value: string) {
553
+ this.classList.__replaceFromString(value);
554
+ this.classList.__syncToElement();
555
+ }
556
+
557
+ get id(): string {
558
+ return typeof this.__props.id === 'string' ? this.__props.id : '';
559
+ }
560
+
561
+ set id(value: string) {
562
+ this.setExactProp('id', value);
563
+ }
564
+
565
+ get textContent(): string {
566
+ return super.textContent;
567
+ }
568
+
569
+ set textContent(value: string) {
570
+ super.textContent = value;
571
+ }
572
+
573
+ get inert(): boolean {
574
+ return this.__props.inert === true;
575
+ }
576
+
577
+ set inert(value: boolean) {
578
+ this.setExactProp('inert', value);
579
+ }
580
+
581
+ get disabled(): boolean {
582
+ return this.__props.disabled === true;
583
+ }
584
+
585
+ set disabled(value: boolean) {
586
+ this.setExactProp('disabled', value);
587
+ }
588
+
589
+ get hidden(): boolean {
590
+ return this.__props.hidden === true;
591
+ }
592
+
593
+ set hidden(value: boolean) {
594
+ this.setExactProp('hidden', value);
595
+ }
596
+
597
+ get tabIndex(): number {
598
+ return typeof this.__props.tabIndex === 'number' ? this.__props.tabIndex : -1;
599
+ }
600
+
601
+ set tabIndex(value: number) {
602
+ this.setExactProp('tabIndex', value);
603
+ }
604
+
605
+ get value(): string {
606
+ return typeof this.__props.value === 'string' ? this.__props.value : '';
607
+ }
608
+
609
+ set value(value: string) {
610
+ this.setExactProp('value', value);
611
+ }
612
+
613
+ get checked(): boolean {
614
+ return this.__props.value === true;
615
+ }
616
+
617
+ set checked(value: boolean) {
618
+ this.setExactProp('value', value);
619
+ }
620
+
621
+ get readOnly(): boolean {
622
+ return this.__props.readOnly === true;
623
+ }
624
+
625
+ set readOnly(value: boolean) {
626
+ this.setExactProp('readOnly', value);
627
+ }
628
+
629
+ cloneNode(deep: boolean = false): ExactElement {
630
+ const clone = new ExactElement(
631
+ this.__state,
632
+ this.ownerDocument,
633
+ this.__tagNameLower,
634
+ cloneProps(this.__props),
635
+ this.namespaceURI,
636
+ );
637
+
638
+ if (deep) {
639
+ for (const child of this.__children) {
640
+ clone.appendChild(child.cloneNode(true) as ExactInsertableNode);
641
+ }
642
+ }
643
+
644
+ return clone;
645
+ }
646
+
647
+ setAttribute(name: string, value: string): void {
648
+ if (this.__state.destroyed) {
649
+ return;
650
+ }
651
+
652
+ const attrName = name === 'className' ? 'class' : name;
653
+
654
+ if (attrName === 'type' && this.__tagNameLower === 'input') {
655
+ this.__props.type = value;
656
+ this.__syncOriginalPropsIfNeeded();
657
+ if (value === 'checkbox' || value === 'radio') {
658
+ this.__recreateBackingNode('toggle');
659
+ } else if (this.__tagConfig.canonicalType === 'toggle') {
660
+ this.__recreateBackingNode('input');
661
+ }
662
+ return;
663
+ }
664
+
665
+ switch (attrName) {
666
+ case 'class':
667
+ this.className = value;
668
+ return;
669
+ case 'style':
670
+ this.style.cssText = value;
671
+ return;
672
+ case 'href':
673
+ if (__DEV__ && this.__tagNameLower === 'a') {
674
+ console.warn('[ExactDOM] <a href> is rendered as a pressable container; href is ignored.');
675
+ }
676
+ return;
677
+ default:
678
+ break;
679
+ }
680
+
681
+ const remapped = ATTR_TO_PROP[this.__tagNameLower]?.[attrName];
682
+ if (remapped) {
683
+ const numericValue = Number(value);
684
+ this.__props[remapped] = Number.isFinite(numericValue) ? numericValue : value;
685
+ } else {
686
+ this.__props[attrName] = value;
687
+ }
688
+
689
+ this.__syncOriginalPropsIfNeeded();
690
+ this.__markDirty();
691
+ }
692
+
693
+ getAttribute(name: string): string | null {
694
+ const attrName = name === 'className' ? 'class' : name;
695
+ if (attrName === 'class') {
696
+ return this.className || null;
697
+ }
698
+ if (attrName === 'style') {
699
+ return this.style.cssText || null;
700
+ }
701
+ const remapped = ATTR_TO_PROP[this.__tagNameLower]?.[attrName];
702
+ const value = this.__props[remapped ?? attrName];
703
+ return value == null ? null : String(value);
704
+ }
705
+
706
+ hasAttribute(name: string): boolean {
707
+ return this.getAttribute(name) !== null;
708
+ }
709
+
710
+ removeAttribute(name: string): void {
711
+ if (this.__state.destroyed) {
712
+ return;
713
+ }
714
+
715
+ const attrName = name === 'className' ? 'class' : name;
716
+
717
+ if (attrName === 'type' && this.__tagNameLower === 'input') {
718
+ delete this.__props.type;
719
+ this.__syncOriginalPropsIfNeeded();
720
+ if (this.__tagConfig.canonicalType === 'toggle') {
721
+ this.__recreateBackingNode('input');
722
+ }
723
+ return;
724
+ }
725
+
726
+ if (attrName === 'class') {
727
+ this.className = '';
728
+ return;
729
+ }
730
+ if (attrName === 'style') {
731
+ this.style.cssText = '';
732
+ return;
733
+ }
734
+
735
+ const remapped = ATTR_TO_PROP[this.__tagNameLower]?.[attrName];
736
+ delete this.__props[remapped ?? attrName];
737
+ this.__syncOriginalPropsIfNeeded();
738
+ this.__markDirty();
739
+ }
740
+
741
+ setAttributeNS(_namespaceURI: string | null, name: string, value: string): void {
742
+ this.setAttribute(name, value);
743
+ }
744
+
745
+ toggleAttribute(name: string, force?: boolean): boolean {
746
+ const shouldAdd = force ?? !this.hasAttribute(name);
747
+ if (shouldAdd) {
748
+ this.setAttribute(name, '');
749
+ return true;
750
+ }
751
+
752
+ this.removeAttribute(name);
753
+ return false;
754
+ }
755
+
756
+ setExactProp(name: string, value: unknown): void {
757
+ if (this.__state.destroyed) {
758
+ return;
759
+ }
760
+ this.__props[name] = value;
761
+ this.__syncOriginalPropsIfNeeded();
762
+ this.__markDirty();
763
+ }
764
+
765
+ removeExactProp(name: string): void {
766
+ if (this.__state.destroyed) {
767
+ return;
768
+ }
769
+ delete this.__props[name];
770
+ this.__syncOriginalPropsIfNeeded();
771
+ this.__markDirty();
772
+ }
773
+
774
+ addEventListener(
775
+ type: string,
776
+ callback: EventListenerOrEventListenerObject | null,
777
+ options?: AddEventListenerOptions | boolean,
778
+ ): void {
779
+ super.addEventListener(type, callback, options);
780
+
781
+ if (!callback) {
782
+ return;
783
+ }
784
+
785
+ const propName = this.__resolveEventProp(type);
786
+ if (!propName) {
787
+ if (__DEV__) {
788
+ console.warn(`[ExactDOM] Unsupported event type "${type}"`);
789
+ }
790
+ return;
791
+ }
792
+
793
+ if (PRESS_EVENT_TYPES.has(type) && !this.__tagConfig.supportsPressEvents) {
794
+ this.__node.pressOverride = true;
795
+ }
796
+
797
+ this.__eventTypes.add(type);
798
+ this.__eventPropNames.set(type, propName);
799
+ this.__props[propName] = this.__getOrCreateEventCallback(type);
800
+ this.__syncOriginalPropsIfNeeded();
801
+ this.__markDirty();
802
+ }
803
+
804
+ removeEventListener(
805
+ type: string,
806
+ callback: EventListenerOrEventListenerObject | null,
807
+ options?: EventListenerOptions | boolean,
808
+ ): void {
809
+ super.removeEventListener(type, callback, options);
810
+
811
+ const previousPropName = this.__eventPropNames.get(type);
812
+ if (!previousPropName) {
813
+ return;
814
+ }
815
+
816
+ if (this.hasListeners(type)) {
817
+ return;
818
+ }
819
+
820
+ delete this.__props[previousPropName];
821
+ this.__eventPropNames.delete(type);
822
+ this.__eventCallbacks.delete(type);
823
+ this.__eventTypes.delete(type);
824
+
825
+ if (
826
+ this.__node.pressOverride === true &&
827
+ !this.__tagConfig.supportsPressEvents &&
828
+ !Array.from(PRESS_EVENT_TYPES).some((eventType) => this.hasListeners(eventType))
829
+ ) {
830
+ this.__node.pressOverride = undefined;
831
+ }
832
+
833
+ this.__syncOriginalPropsIfNeeded();
834
+ this.__markDirty();
835
+ }
836
+
837
+ __syncOriginalPropsIfNeeded(): void {
838
+ if (!isCreatedNode(this.__node)) {
839
+ return;
840
+ }
841
+ this.__node.originalProps = cloneProps(this.__props);
842
+ }
843
+
844
+ __markDirty(): void {
845
+ this.__state.__markDirty(this);
846
+ }
847
+
848
+ __prepareForCommit(): void {
849
+ // The style proxy mutates its own bag in-place. Snapshot it immediately
850
+ // before diffing so host-ops sees one coherent style object per commit.
851
+ this.__props.style = this.style.__snapshot();
852
+ }
853
+
854
+ __getOrCreateEventCallback(domType: string): Function {
855
+ const existing = this.__eventCallbacks.get(domType);
856
+ if (existing) {
857
+ return existing;
858
+ }
859
+
860
+ const callback = (payload: unknown) => {
861
+ this.__syncNativeValue(domType, payload);
862
+ const event = this.__wrapPayload(domType, payload);
863
+ const nativePreventDefault = (event as Event & {
864
+ __nativePreventDefault?: (() => void) | undefined;
865
+ }).__nativePreventDefault;
866
+
867
+ if (nativePreventDefault) {
868
+ const originalPreventDefault = event.preventDefault.bind(event);
869
+ event.preventDefault = () => {
870
+ originalPreventDefault();
871
+ if (event.defaultPrevented) {
872
+ nativePreventDefault();
873
+ }
874
+ };
875
+ }
876
+
877
+ this.dispatchEvent(event);
878
+ };
879
+
880
+ this.__eventCallbacks.set(domType, callback);
881
+ return callback;
882
+ }
883
+
884
+ __resolveEventProp(type: string): string | null {
885
+ switch (type) {
886
+ case 'click':
887
+ return 'onPress';
888
+ case 'pointerdown':
889
+ return 'onPressIn';
890
+ case 'pointerup':
891
+ return 'onPressOut';
892
+ case 'focus':
893
+ return 'onFocus';
894
+ case 'blur':
895
+ return 'onBlur';
896
+ case 'focusin':
897
+ return 'onFocusIn';
898
+ case 'focusout':
899
+ return 'onFocusOut';
900
+ case 'keydown':
901
+ return 'onKeyDown';
902
+ case 'keyup':
903
+ return 'onKeyUp';
904
+ case 'change':
905
+ return this.__tagConfig.canonicalType === 'toggle' ? 'onValueChange' : 'onChange';
906
+ case 'input':
907
+ return 'onChangeText';
908
+ default:
909
+ return null;
910
+ }
911
+ }
912
+
913
+ __syncEventPropsFromListeners(): void {
914
+ for (const propName of this.__eventPropNames.values()) {
915
+ delete this.__props[propName];
916
+ }
917
+ this.__eventPropNames.clear();
918
+ this.__eventCallbacks.clear();
919
+
920
+ for (const eventType of this.__eventTypes) {
921
+ if (!this.hasListeners(eventType)) {
922
+ continue;
923
+ }
924
+
925
+ const propName = this.__resolveEventProp(eventType);
926
+ if (!propName) {
927
+ continue;
928
+ }
929
+
930
+ this.__eventPropNames.set(eventType, propName);
931
+ this.__props[propName] = this.__getOrCreateEventCallback(eventType);
932
+ }
933
+
934
+ this.__syncOriginalPropsIfNeeded();
935
+ }
936
+
937
+ __syncNativeValue(domType: string, payload: unknown): void {
938
+ switch (domType) {
939
+ case 'input':
940
+ if (typeof payload === 'string') {
941
+ this.__props.value = payload;
942
+ }
943
+ break;
944
+ case 'change':
945
+ if (this.__tagConfig.canonicalType === 'toggle') {
946
+ if (typeof payload === 'boolean') {
947
+ this.__props.value = payload;
948
+ } else if (typeof payload === 'object' && payload !== null) {
949
+ const value = (payload as { nativeEvent?: { value?: unknown } }).nativeEvent?.value;
950
+ if (typeof value === 'boolean') {
951
+ this.__props.value = value;
952
+ }
953
+ }
954
+ } else if (typeof payload === 'object' && payload !== null) {
955
+ const text =
956
+ (payload as { nativeEvent?: { text?: unknown }; text?: unknown }).nativeEvent?.text ??
957
+ (payload as { nativeEvent?: { text?: unknown }; text?: unknown }).text;
958
+ if (typeof text === 'string') {
959
+ this.__props.value = text;
960
+ }
961
+ }
962
+ break;
963
+ default:
964
+ break;
965
+ }
966
+ }
967
+
968
+ __wrapPayload(domType: string, payload: unknown): Event {
969
+ switch (domType) {
970
+ case 'keydown':
971
+ case 'keyup': {
972
+ const nativeEvent =
973
+ (payload as {
974
+ nativeEvent?: {
975
+ key?: unknown;
976
+ code?: unknown;
977
+ altKey?: unknown;
978
+ ctrlKey?: unknown;
979
+ metaKey?: unknown;
980
+ shiftKey?: unknown;
981
+ repeat?: unknown;
982
+ };
983
+ preventDefault?: unknown;
984
+ } | null)?.nativeEvent ?? {};
985
+
986
+ const event = new KeyboardEvent(domType, {
987
+ key: typeof nativeEvent.key === 'string' ? nativeEvent.key : '',
988
+ code: typeof nativeEvent.code === 'string' ? nativeEvent.code : '',
989
+ altKey: nativeEvent.altKey === true,
990
+ ctrlKey: nativeEvent.ctrlKey === true,
991
+ metaKey: nativeEvent.metaKey === true,
992
+ shiftKey: nativeEvent.shiftKey === true,
993
+ repeat: nativeEvent.repeat === true,
994
+ cancelable: true,
995
+ bubbles: false,
996
+ }) as KeyboardEvent & {
997
+ __nativePreventDefault?: (() => void) | undefined;
998
+ };
999
+
1000
+ event.__nativePreventDefault =
1001
+ typeof (payload as { preventDefault?: unknown } | null)?.preventDefault === 'function'
1002
+ ? (payload as { preventDefault: () => void }).preventDefault
1003
+ : undefined;
1004
+ return event;
1005
+ }
1006
+ case 'focus':
1007
+ case 'blur':
1008
+ case 'focusin':
1009
+ case 'focusout': {
1010
+ const relatedTargetId =
1011
+ (payload as {
1012
+ nativeEvent?: { relatedTarget?: unknown };
1013
+ relatedTarget?: unknown;
1014
+ } | null)?.nativeEvent?.relatedTarget ??
1015
+ (payload as {
1016
+ nativeEvent?: { relatedTarget?: unknown };
1017
+ relatedTarget?: unknown;
1018
+ } | null)?.relatedTarget;
1019
+
1020
+ return new FocusEvent(domType, {
1021
+ relatedTarget:
1022
+ typeof relatedTargetId === 'number'
1023
+ ? ((this.__state.__getWrapperById(relatedTargetId) as unknown as globalThis.EventTarget | null) ?? null)
1024
+ : null,
1025
+ cancelable: false,
1026
+ bubbles: false,
1027
+ });
1028
+ }
1029
+ case 'click':
1030
+ case 'pointerdown':
1031
+ case 'pointerup':
1032
+ return new Event(domType, {
1033
+ cancelable: true,
1034
+ bubbles: false,
1035
+ });
1036
+ case 'input': {
1037
+ const event = new Event(domType, {
1038
+ cancelable: false,
1039
+ bubbles: false,
1040
+ }) as Event & { nativeEvent?: unknown };
1041
+ event.nativeEvent = {
1042
+ value: typeof payload === 'string' ? payload : '',
1043
+ };
1044
+ return event;
1045
+ }
1046
+ case 'change': {
1047
+ const event = new Event(domType, {
1048
+ cancelable: false,
1049
+ bubbles: false,
1050
+ }) as Event & { nativeEvent?: unknown };
1051
+ if (this.__tagConfig.canonicalType === 'toggle') {
1052
+ event.nativeEvent = {
1053
+ value:
1054
+ typeof payload === 'boolean'
1055
+ ? payload
1056
+ : (
1057
+ payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null
1058
+ )?.nativeEvent?.value ??
1059
+ (
1060
+ payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null
1061
+ )?.value ??
1062
+ false,
1063
+ };
1064
+ } else {
1065
+ event.nativeEvent = payload;
1066
+ }
1067
+ return event;
1068
+ }
1069
+ default: {
1070
+ const event = new Event(domType, {
1071
+ cancelable: CANCELABLE_EVENTS.has(domType),
1072
+ bubbles: false,
1073
+ }) as Event & { nativeEvent?: unknown };
1074
+ event.nativeEvent = payload;
1075
+ return event;
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ __recreateBackingNode(newTag: string): void {
1081
+ const parent = this.__parent instanceof ExactContainerNodeBase ? this.__parent : null;
1082
+ const currentIndex = parent ? parent.__children.indexOf(this) : -1;
1083
+ const nextRenderableSibling =
1084
+ parent && currentIndex !== -1
1085
+ ? findFirstRenderableFromIndex(parent.__children, currentIndex + 1)
1086
+ : null;
1087
+ const wasAttached = parent ? isParentAttachedToHost(parent) : false;
1088
+ const oldNode = this.__node;
1089
+
1090
+ if (wasAttached && parent) {
1091
+ const hostParent = parent.__getHostParent();
1092
+ if (hostParent) {
1093
+ hostRemoveChild(hostParent, oldNode);
1094
+ this.__state.__noteCommitNeeded();
1095
+ }
1096
+ }
1097
+
1098
+ // Event prop names are tag-aware (`change` maps to `onChange` for text
1099
+ // inputs but `onValueChange` for toggles). Rebuild the prop bag against the
1100
+ // new tag config *before* creating the new host node so the initial
1101
+ // CreateView payload is encoded with the correct handlers.
1102
+ this.__tagConfig = getTagConfig(newTag) ?? defaultTagConfig;
1103
+ this.__syncEventPropsFromListeners();
1104
+
1105
+ this.__state.__unregisterWrapper(oldNode.id);
1106
+ this.__node = createInstance(newTag, cloneProps(this.__props));
1107
+ this.__state.__registerWrapper(this.__node.id, this);
1108
+
1109
+ if (wasAttached && parent) {
1110
+ const hostParent = parent.__getHostParent();
1111
+ if (hostParent) {
1112
+ if (nextRenderableSibling) {
1113
+ hostInsertBefore(hostParent, this.__node, nextRenderableSibling.__node);
1114
+ } else {
1115
+ hostAppendChild(hostParent, this.__node);
1116
+ }
1117
+ this.__state.__noteCommitNeeded();
1118
+ }
1119
+ }
1120
+
1121
+ this.__lastCommittedProps = cloneProps(this.__props);
1122
+ this.__state.__clearDirty(this);
1123
+ }
1124
+ }
1125
+
1126
+ export class ExactText extends ExactNodeBase {
1127
+ readonly nodeType = 3;
1128
+ readonly nodeName = '#text';
1129
+ readonly __node: TextNode;
1130
+ __pendingText: string | null = null;
1131
+
1132
+ constructor(state: ExactDOMState, ownerDocument: ExactDocument, value: string) {
1133
+ super(state, ownerDocument);
1134
+ this.__node = createTextInstance(value);
1135
+ state.__registerWrapper(this.__node.id, this);
1136
+ }
1137
+
1138
+ get data(): string {
1139
+ return this.__node.text;
1140
+ }
1141
+
1142
+ set data(value: string) {
1143
+ this.__node.text = value;
1144
+
1145
+ if (this.__node.parent === null) {
1146
+ this.__pendingText = isCreatedNode(this.__node) ? null : value;
1147
+ return;
1148
+ }
1149
+
1150
+ updateTextContent(this.__node, value);
1151
+ this.__state.__noteCommitNeeded();
1152
+ }
1153
+
1154
+ get textContent(): string {
1155
+ return this.__pendingText ?? this.__node.text;
1156
+ }
1157
+
1158
+ set textContent(value: string) {
1159
+ this.data = value;
1160
+ }
1161
+
1162
+ get nodeValue(): string {
1163
+ return this.textContent;
1164
+ }
1165
+
1166
+ set nodeValue(value: string) {
1167
+ this.data = value;
1168
+ }
1169
+
1170
+ cloneNode(): ExactText {
1171
+ return new ExactText(this.__state, this.ownerDocument, this.textContent);
1172
+ }
1173
+
1174
+ __flushPendingText(): void {
1175
+ if (this.__pendingText === null || this.__node.parent === null) {
1176
+ return;
1177
+ }
1178
+ updateTextContent(this.__node, this.__pendingText);
1179
+ this.__pendingText = null;
1180
+ this.__state.__noteCommitNeeded();
1181
+ }
1182
+ }
1183
+
1184
+ export class ExactComment extends ExactNodeBase {
1185
+ readonly nodeType = 8;
1186
+ readonly nodeName = '#comment';
1187
+ data: string;
1188
+
1189
+ constructor(state: ExactDOMState, ownerDocument: ExactDocument, value: string) {
1190
+ super(state, ownerDocument);
1191
+ this.data = value;
1192
+ }
1193
+
1194
+ get textContent(): string {
1195
+ return this.data;
1196
+ }
1197
+
1198
+ set textContent(value: string) {
1199
+ this.data = value;
1200
+ }
1201
+
1202
+ get nodeValue(): string {
1203
+ return this.data;
1204
+ }
1205
+
1206
+ set nodeValue(value: string) {
1207
+ this.data = value;
1208
+ }
1209
+
1210
+ cloneNode(): ExactComment {
1211
+ return new ExactComment(this.__state, this.ownerDocument, this.data);
1212
+ }
1213
+ }
1214
+
1215
+ export class ExactDocumentFragment extends ExactContainerNodeBase {
1216
+ readonly nodeType = 11;
1217
+ readonly nodeName = '#document-fragment';
1218
+
1219
+ __getHostParent(): ElementNode | RootNode | null {
1220
+ return null;
1221
+ }
1222
+
1223
+ cloneNode(deep: boolean = false): ExactDocumentFragment {
1224
+ const clone = new ExactDocumentFragment(this.__state, this.ownerDocument);
1225
+ if (deep) {
1226
+ for (const child of this.__children) {
1227
+ clone.appendChild(child.cloneNode(true) as ExactInsertableNode);
1228
+ }
1229
+ }
1230
+ return clone;
1231
+ }
1232
+ }
1233
+
1234
+ export class ExactRootBodyElement extends ExactContainerNodeBase {
1235
+ readonly nodeType = 1;
1236
+ readonly tagName = 'BODY';
1237
+ readonly nodeName = 'BODY';
1238
+
1239
+ __getHostParent(): ElementNode | RootNode | null {
1240
+ return this.__state.root;
1241
+ }
1242
+ }
1243
+
1244
+ export class ExactDocumentElement extends ExactNodeBase {
1245
+ readonly nodeType = 1;
1246
+ readonly tagName = 'HTML';
1247
+ readonly nodeName = 'HTML';
1248
+ get childNodes(): ExactChildNode[] {
1249
+ return [this.ownerDocument.body as unknown as ExactChildNode];
1250
+ }
1251
+
1252
+ get firstChild(): ExactChildNode | null {
1253
+ return this.ownerDocument.body as unknown as ExactChildNode;
1254
+ }
1255
+
1256
+ get lastChild(): ExactChildNode | null {
1257
+ return this.ownerDocument.body as unknown as ExactChildNode;
1258
+ }
1259
+
1260
+ get textContent(): string {
1261
+ return this.ownerDocument.body.textContent;
1262
+ }
1263
+ }
1264
+
1265
+ export class ExactWindow extends EventTarget {
1266
+ readonly __state: ExactDOMState;
1267
+ readonly navigator = {
1268
+ userAgent: 'ExactDOM/1.0',
1269
+ };
1270
+ readonly document: ExactDocument;
1271
+
1272
+ constructor(state: ExactDOMState, document: ExactDocument) {
1273
+ super();
1274
+ this.__state = state;
1275
+ this.document = document;
1276
+ }
1277
+
1278
+ get innerWidth(): number {
1279
+ return readViewport(this.__state.rootId).width;
1280
+ }
1281
+
1282
+ get innerHeight(): number {
1283
+ return readViewport(this.__state.rootId).height;
1284
+ }
1285
+
1286
+ requestAnimationFrame(callback: FrameRequestCallback): number {
1287
+ if (typeof globalThis.requestAnimationFrame === 'function') {
1288
+ return globalThis.requestAnimationFrame(callback);
1289
+ }
1290
+ return globalThis.setTimeout(() => callback(Date.now()), 16) as unknown as number;
1291
+ }
1292
+
1293
+ cancelAnimationFrame(handle: number): void {
1294
+ if (typeof globalThis.cancelAnimationFrame === 'function') {
1295
+ globalThis.cancelAnimationFrame(handle);
1296
+ return;
1297
+ }
1298
+ globalThis.clearTimeout(handle);
1299
+ }
1300
+
1301
+ getComputedStyle(): { getPropertyValue: (name: string) => string } {
1302
+ if (__DEV__) {
1303
+ console.warn('[ExactDOM] getComputedStyle() is a stub in the DOM shim.');
1304
+ }
1305
+ return {
1306
+ getPropertyValue: () => '',
1307
+ };
1308
+ }
1309
+
1310
+ __dispatchResize(): void {
1311
+ this.dispatchEvent(new Event('resize'));
1312
+ }
1313
+ }
1314
+
1315
+ export class ExactDocument extends EventTarget {
1316
+ readonly __state: ExactDOMState;
1317
+ readonly body: ExactRootBodyElement;
1318
+ readonly head: ExactDocumentFragment;
1319
+ readonly documentElement: ExactDocumentElement;
1320
+ defaultView: ExactWindow | null = null;
1321
+
1322
+ constructor(state: ExactDOMState) {
1323
+ super();
1324
+ this.__state = state;
1325
+ this.body = new ExactRootBodyElement(state, this);
1326
+ this.head = new ExactDocumentFragment(state, this);
1327
+ this.documentElement = new ExactDocumentElement(state, this);
1328
+ this.body.__parent = this.documentElement;
1329
+ }
1330
+
1331
+ createElement(tagName: string): ExactElement {
1332
+ const defaultProps = TAG_DEFAULTS[tagName.toLowerCase()] ?? {};
1333
+ return new ExactElement(this.__state, this, tagName, cloneProps(defaultProps));
1334
+ }
1335
+
1336
+ createElementNS(namespaceURI: string | null, tagName: string): ExactElement {
1337
+ // SVG support in the shim is intentionally shallow, but DOM-targeting
1338
+ // frameworks still expect `createElementNS` to exist. We keep the element
1339
+ // creation path unified so tag-map resolution, defaults, and dirty
1340
+ // tracking behave exactly the same as `createElement`.
1341
+ const defaultProps = TAG_DEFAULTS[tagName.toLowerCase()] ?? {};
1342
+ return new ExactElement(
1343
+ this.__state,
1344
+ this,
1345
+ tagName,
1346
+ cloneProps(defaultProps),
1347
+ namespaceURI,
1348
+ );
1349
+ }
1350
+
1351
+ createTextNode(value: string): ExactText {
1352
+ return new ExactText(this.__state, this, value);
1353
+ }
1354
+
1355
+ createComment(value: string): ExactComment {
1356
+ return new ExactComment(this.__state, this, value);
1357
+ }
1358
+
1359
+ createDocumentFragment(): ExactDocumentFragment {
1360
+ return new ExactDocumentFragment(this.__state, this);
1361
+ }
1362
+
1363
+ importNode<T extends ExactChildNode | ExactDocumentFragment>(node: T, deep: boolean = false): T {
1364
+ return node.cloneNode(deep) as T;
1365
+ }
1366
+
1367
+ querySelector(selector: string): ExactElement | null {
1368
+ return this.head.querySelector(selector) ?? this.body.querySelector(selector);
1369
+ }
1370
+ }
1371
+
1372
+ class ExactDOMState {
1373
+ readonly rootId: number;
1374
+ readonly root: RootNode;
1375
+ readonly wrappers = new Map<number, ExactRenderableNode>();
1376
+ readonly dirtyElements = new Set<ExactElement>();
1377
+ document!: ExactDocument;
1378
+ window!: ExactWindow;
1379
+ destroyed = false;
1380
+ flushScheduled = false;
1381
+ needsCommit = false;
1382
+ pendingRootSync = false;
1383
+ pendingResize = false;
1384
+ lastViewport: { width: number; height: number };
1385
+ unsubscribeWindowState: Unsubscribe | null = null;
1386
+
1387
+ constructor(rootId: number) {
1388
+ this.rootId = rootId;
1389
+ this.root = createRoot(rootId, 'dom-shim');
1390
+ this.lastViewport = readViewport(rootId);
1391
+ }
1392
+
1393
+ __registerWrapper(id: number, wrapper: ExactRenderableNode): void {
1394
+ this.wrappers.set(id, wrapper);
1395
+ }
1396
+
1397
+ __unregisterWrapper(id: number): void {
1398
+ this.wrappers.delete(id);
1399
+ }
1400
+
1401
+ __getWrapperById(id: number): ExactRenderableNode | null {
1402
+ return this.wrappers.get(id) ?? null;
1403
+ }
1404
+
1405
+ __markDirty(element: ExactElement): void {
1406
+ this.dirtyElements.add(element);
1407
+ this.needsCommit = true;
1408
+ this.__scheduleFlush();
1409
+ }
1410
+
1411
+ __clearDirty(element: ExactElement): void {
1412
+ this.dirtyElements.delete(element);
1413
+ }
1414
+
1415
+ __noteCommitNeeded(): void {
1416
+ this.needsCommit = true;
1417
+ this.__scheduleFlush();
1418
+ }
1419
+
1420
+ __scheduleRootSync(resizeChanged: boolean): void {
1421
+ this.pendingRootSync = true;
1422
+ this.pendingResize ||= resizeChanged;
1423
+ this.__scheduleFlush();
1424
+ }
1425
+
1426
+ __scheduleFlush(): void {
1427
+ if (this.destroyed || this.flushScheduled) {
1428
+ return;
1429
+ }
1430
+
1431
+ this.flushScheduled = true;
1432
+ queueMicrotask(() => {
1433
+ this.flushScheduled = false;
1434
+ this.__flush();
1435
+ });
1436
+ }
1437
+
1438
+ __flush(): void {
1439
+ if (this.destroyed) {
1440
+ return;
1441
+ }
1442
+
1443
+ const stillDirty = new Set<ExactElement>();
1444
+
1445
+ for (const element of this.dirtyElements) {
1446
+ if (this.destroyed) {
1447
+ return;
1448
+ }
1449
+
1450
+ if (element.__node.parent === null) {
1451
+ stillDirty.add(element);
1452
+ continue;
1453
+ }
1454
+
1455
+ element.__prepareForCommit();
1456
+ updateInstanceProps(
1457
+ element.__node,
1458
+ element.__lastCommittedProps,
1459
+ element.__props,
1460
+ );
1461
+ element.__lastCommittedProps = cloneProps(element.__props);
1462
+ }
1463
+
1464
+ this.dirtyElements.clear();
1465
+ for (const element of stillDirty) {
1466
+ this.dirtyElements.add(element);
1467
+ }
1468
+
1469
+ if (this.pendingRootSync) {
1470
+ this.pendingRootSync = false;
1471
+ syncRootWindowState(this.root);
1472
+ this.needsCommit = false;
1473
+ } else if (this.needsCommit) {
1474
+ this.needsCommit = false;
1475
+ commitBatch(this.root);
1476
+ }
1477
+
1478
+ if (this.pendingResize) {
1479
+ this.pendingResize = false;
1480
+ this.window.__dispatchResize();
1481
+ }
1482
+ }
1483
+
1484
+ destroy(): void {
1485
+ if (this.destroyed) {
1486
+ return;
1487
+ }
1488
+
1489
+ this.destroyed = true;
1490
+ this.unsubscribeWindowState?.();
1491
+ this.unsubscribeWindowState = null;
1492
+ this.dirtyElements.clear();
1493
+ this.wrappers.clear();
1494
+ this.document.body.__children = [];
1495
+ destroyRoot(this.rootId);
1496
+ }
1497
+ }
1498
+
1499
+ function findFirstRenderableFromIndex(
1500
+ nodes: ExactChildNode[],
1501
+ startIndex: number,
1502
+ ): ExactRenderableNode | null {
1503
+ for (let index = startIndex; index < nodes.length; index += 1) {
1504
+ const child = nodes[index];
1505
+ if (child && isRenderableNode(child)) {
1506
+ return child;
1507
+ }
1508
+ }
1509
+ return null;
1510
+ }
1511
+
1512
+ function normalizeAppendableNode(
1513
+ ownerDocument: ExactDocument,
1514
+ node: ExactAppendableNode,
1515
+ ): ExactInsertableNode {
1516
+ return typeof node === 'string' ? ownerDocument.createTextNode(node) : node;
1517
+ }
1518
+
1519
+ function querySelectorInChildren(
1520
+ children: ExactChildNode[],
1521
+ selector: string,
1522
+ ): ExactElement | null {
1523
+ const isIdSelector = selector.startsWith('#');
1524
+ const id = isIdSelector ? selector.slice(1) : null;
1525
+ const tag = isIdSelector ? null : selector.toUpperCase();
1526
+
1527
+ for (const child of children) {
1528
+ if (child instanceof ExactElement) {
1529
+ if (id !== null && child.id === id) {
1530
+ return child;
1531
+ }
1532
+ if (tag !== null && child.tagName === tag) {
1533
+ return child;
1534
+ }
1535
+
1536
+ const nested = querySelectorInChildren(child.__children, selector);
1537
+ if (nested) {
1538
+ return nested;
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ return null;
1544
+ }
1545
+
1546
+ function isParentAttachedToHost(parent: ExactParentNode): boolean {
1547
+ if (parent instanceof ExactRootBodyElement) {
1548
+ return !parent.__state.destroyed;
1549
+ }
1550
+ if (parent instanceof ExactDocumentFragment) {
1551
+ return false;
1552
+ }
1553
+ return parent.__node.parent !== null;
1554
+ }
1555
+
1556
+ function ensureSameRoot(parent: ExactParentNode, child: ExactInsertableNode): void {
1557
+ if (child.__rootId !== parent.__rootId) {
1558
+ throw new Error('[ExactDOM] Cannot move a node between different DOM roots.');
1559
+ }
1560
+ }
1561
+
1562
+ function removeNodeFromParent(parent: ExactParentNode, child: ExactChildNode): ExactChildNode {
1563
+ const index = parent.__children.indexOf(child);
1564
+ if (index === -1) {
1565
+ throw new Error('[ExactDOM] removeChild: node is not a child of this parent.');
1566
+ }
1567
+
1568
+ parent.__children.splice(index, 1);
1569
+ child.__parent = null;
1570
+
1571
+ if (isRenderableNode(child) && isParentAttachedToHost(parent)) {
1572
+ const hostParent = parent.__getHostParent();
1573
+ if (hostParent) {
1574
+ hostDetachChild(hostParent, child.__node);
1575
+ parent.__state.__noteCommitNeeded();
1576
+ }
1577
+ } else if (isRenderableNode(child) && parent instanceof ExactElement) {
1578
+ // Detached DOM trees still need their backing Exact-node structure kept in
1579
+ // sync so a later first attachment can encode the full subtree correctly.
1580
+ hostTreeRemoveChild(parent.__node, child.__node);
1581
+ }
1582
+
1583
+ return child;
1584
+ }
1585
+
1586
+ function primeCreatedElementSubtree(element: ExactElement): void {
1587
+ // Created detached nodes keep `originalProps` synchronized as props change so
1588
+ // the eventual CreateView payload sees the latest values. Event bindings,
1589
+ // however, live on `instance.events`, so we prime them explicitly right
1590
+ // before the first host attachment of the subtree.
1591
+ element.__prepareForCommit();
1592
+ element.__node.originalProps = cloneProps(element.__props);
1593
+ processEventProps(element.__node, element.__props, element.__tagConfig);
1594
+
1595
+ for (const child of element.__children) {
1596
+ if (child instanceof ExactElement && isCreatedNode(child.__node)) {
1597
+ primeCreatedElementSubtree(child);
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ function finalizeCreatedElementSubtree(
1603
+ state: ExactDOMState,
1604
+ element: ExactElement,
1605
+ ): void {
1606
+ // The subtree was just emitted via `encodeCreatedSubtree`, so its current
1607
+ // props now represent the committed baseline. Clear the dirty bookkeeping to
1608
+ // avoid an immediate duplicate diff on the next microtask.
1609
+ element.__lastCommittedProps = cloneProps(element.__props);
1610
+ state.__clearDirty(element);
1611
+
1612
+ for (const child of element.__children) {
1613
+ if (child instanceof ExactElement && !isCreatedNode(child.__node)) {
1614
+ finalizeCreatedElementSubtree(state, child);
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ function insertNodeIntoParent(
1620
+ parent: ExactParentNode,
1621
+ child: ExactInsertableNode,
1622
+ beforeChild: ExactChildNode | null,
1623
+ ): ExactInsertableNode {
1624
+ if (parent.__state.destroyed) {
1625
+ return child;
1626
+ }
1627
+
1628
+ if (child instanceof ExactDocumentFragment) {
1629
+ const children = child.__children.slice();
1630
+ for (const fragmentChild of children) {
1631
+ insertNodeIntoParent(parent, fragmentChild, beforeChild);
1632
+ }
1633
+ child.__children = [];
1634
+ return child;
1635
+ }
1636
+
1637
+ ensureSameRoot(parent, child);
1638
+
1639
+ if (beforeChild && beforeChild.__parent !== parent) {
1640
+ throw new Error('[ExactDOM] insertBefore: reference node is not a child of this parent.');
1641
+ }
1642
+
1643
+ if (child === beforeChild) {
1644
+ return child;
1645
+ }
1646
+
1647
+ if (child.__parent instanceof ExactContainerNodeBase) {
1648
+ removeNodeFromParent(child.__parent, child);
1649
+ }
1650
+
1651
+ const insertionIndex =
1652
+ beforeChild == null ? parent.__children.length : parent.__children.indexOf(beforeChild);
1653
+ parent.__children.splice(insertionIndex, 0, child);
1654
+ child.__parent = parent;
1655
+
1656
+ if (isRenderableNode(child) && isParentAttachedToHost(parent)) {
1657
+ const wasCreatedElement = child instanceof ExactElement && isCreatedNode(child.__node);
1658
+ const hostParent = parent.__getHostParent();
1659
+ if (hostParent) {
1660
+ if (wasCreatedElement) {
1661
+ primeCreatedElementSubtree(child);
1662
+ }
1663
+
1664
+ const renderableAnchor = findFirstRenderableFromIndex(
1665
+ parent.__children,
1666
+ insertionIndex + 1,
1667
+ );
1668
+ if (renderableAnchor) {
1669
+ hostInsertBefore(hostParent, child.__node, renderableAnchor.__node);
1670
+ } else {
1671
+ hostAppendChild(hostParent, child.__node);
1672
+ }
1673
+
1674
+ if (child instanceof ExactText) {
1675
+ child.__flushPendingText();
1676
+ } else if (wasCreatedElement) {
1677
+ finalizeCreatedElementSubtree(parent.__state, child);
1678
+ }
1679
+
1680
+ parent.__state.__noteCommitNeeded();
1681
+ }
1682
+ } else if (isRenderableNode(child) && parent instanceof ExactElement) {
1683
+ // Keep the detached backing tree structurally accurate even before the
1684
+ // subtree is attached to a real Exact root. Svelte, in particular, builds
1685
+ // and clones DOM subtrees off-screen and expects that later first
1686
+ // attachment will preserve those descendants.
1687
+ const renderableAnchor = findFirstRenderableFromIndex(parent.__children, insertionIndex + 1);
1688
+ if (renderableAnchor) {
1689
+ hostTreeInsertBefore(parent.__node, child.__node, renderableAnchor.__node);
1690
+ } else {
1691
+ hostTreeAppendChild(parent.__node, child.__node);
1692
+ }
1693
+ }
1694
+
1695
+ return child;
1696
+ }
1697
+
1698
+ export interface ExactDOMHandle {
1699
+ readonly rootId: number;
1700
+ readonly document: ExactDocument;
1701
+ readonly window: ExactWindow;
1702
+ destroy(): void;
1703
+ reset(): ExactDOMHandle;
1704
+ }
1705
+
1706
+ export function createExactDOM(options: { rootId?: number } = {}): ExactDOMHandle {
1707
+ const rootId = options.rootId ?? 0;
1708
+ const state = new ExactDOMState(rootId);
1709
+ const document = new ExactDocument(state);
1710
+ const window = new ExactWindow(state, document);
1711
+ document.defaultView = window;
1712
+ state.document = document;
1713
+ state.window = window;
1714
+
1715
+ state.unsubscribeWindowState = subscribeToRootWindowState(rootId, () => {
1716
+ const nextViewport = readViewport(rootId);
1717
+ const resizeChanged =
1718
+ nextViewport.width !== state.lastViewport.width ||
1719
+ nextViewport.height !== state.lastViewport.height;
1720
+ state.lastViewport = nextViewport;
1721
+ state.__scheduleRootSync(resizeChanged);
1722
+ });
1723
+
1724
+ return {
1725
+ rootId,
1726
+ document,
1727
+ window,
1728
+ destroy(): void {
1729
+ state.destroy();
1730
+ },
1731
+ reset(): ExactDOMHandle {
1732
+ state.destroy();
1733
+ return createExactDOM({ rootId });
1734
+ },
1735
+ };
1736
+ }