specnav-core 0.2.4 → 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/morpher.js CHANGED
@@ -1,5 +1,128 @@
1
1
  'use strict';
2
2
 
3
+ // src/morpher/fingerprint.ts
4
+ function fnv1a(str) {
5
+ let hash = 2166136261;
6
+ for (let i = 0; i < str.length; i++) {
7
+ hash ^= str.charCodeAt(i);
8
+ hash = hash * 16777619 >>> 0;
9
+ }
10
+ return hash.toString(36);
11
+ }
12
+ function fingerprintNode(node) {
13
+ const tag = node.tagName.toLowerCase();
14
+ const attrs = Array.from(node.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("|");
15
+ const text = node.childNodes.length === 0 ? node.textContent ?? "" : "";
16
+ const childHashes = Array.from(node.children).map(fingerprintNode).join(",");
17
+ return fnv1a(`${tag}[${attrs}]{${text}}(${childHashes})`);
18
+ }
19
+ var FINGERPRINT_KEY = "__specnav_fp__";
20
+ function stampFingerprints(root) {
21
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
22
+ let node = walker.currentNode;
23
+ while (node) {
24
+ node[FINGERPRINT_KEY] = fingerprintNode(node);
25
+ node = walker.nextNode();
26
+ }
27
+ }
28
+ function fingerprintMatches(current, incoming) {
29
+ const currentFp = current[FINGERPRINT_KEY];
30
+ if (!currentFp) return false;
31
+ return currentFp === fingerprintNode(incoming);
32
+ }
33
+
34
+ // src/morpher/identity.ts
35
+ function fnv1a2(str) {
36
+ let hash = 2166136261;
37
+ for (let i = 0; i < str.length; i++) {
38
+ hash ^= str.charCodeAt(i);
39
+ hash = hash * 16777619 >>> 0;
40
+ }
41
+ return hash.toString(36);
42
+ }
43
+ function buildIdentityVector(node, depth = 0) {
44
+ const classes = Array.from(node.classList).sort().join(" ");
45
+ const volatileAttrs = /* @__PURE__ */ new Set(["style", "class", "aria-expanded", "aria-selected"]);
46
+ const stableAttrs = Array.from(node.attributes).filter((a) => !volatileAttrs.has(a.name) && a.name !== "id").sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("|");
47
+ return {
48
+ tagName: node.tagName.toLowerCase(),
49
+ id: node.id || null,
50
+ key: node.getAttribute("data-key"),
51
+ classSignature: fnv1a2(classes),
52
+ attrSignature: fnv1a2(stableAttrs),
53
+ textFingerprint: node.children.length === 0 ? (node.textContent ?? "").slice(0, 64) : null,
54
+ childCount: node.childElementCount,
55
+ childTagSequence: Array.from(node.children).slice(0, 5).map((c) => c.tagName.toLowerCase()).join(">"),
56
+ domPosition: Array.from(node.parentElement?.children ?? []).indexOf(node),
57
+ role: node.getAttribute("role")
58
+ };
59
+ }
60
+ function identityScore(a, b) {
61
+ if (a.tagName !== b.tagName) return 0;
62
+ let score = 0;
63
+ let totalWeight = 0;
64
+ const checks = [
65
+ [a.id, b.id, 40],
66
+ [a.key, b.key, 35],
67
+ [a.classSignature, b.classSignature, 15],
68
+ [a.attrSignature, b.attrSignature, 20],
69
+ [a.textFingerprint, b.textFingerprint, 15],
70
+ [a.childCount, b.childCount, 10],
71
+ [a.childTagSequence, b.childTagSequence, 15],
72
+ [a.domPosition, b.domPosition, 5],
73
+ [a.role, b.role, 10]
74
+ ];
75
+ for (const [av, bv, weight] of checks) {
76
+ if (av !== null && bv !== null) {
77
+ totalWeight += weight;
78
+ if (av === bv) score += weight;
79
+ }
80
+ }
81
+ return totalWeight === 0 ? 0 : score / totalWeight;
82
+ }
83
+
84
+ // src/morpher/matcher.ts
85
+ function matchSiblings(currentChildren, incomingChildren) {
86
+ const n = currentChildren.length;
87
+ const m = incomingChildren.length;
88
+ const THRESHOLD = 0.4;
89
+ const scores = Array.from(
90
+ { length: n },
91
+ (_, i) => Array.from(
92
+ { length: m },
93
+ (_2, j) => identityScore(
94
+ buildIdentityVector(currentChildren[i]),
95
+ buildIdentityVector(incomingChildren[j])
96
+ )
97
+ )
98
+ );
99
+ const matched = /* @__PURE__ */ new Map();
100
+ const usedIncoming = /* @__PURE__ */ new Set();
101
+ const order = currentChildren.map((_, i) => i).sort((a, b) => {
102
+ const bestA = Math.max(...scores[a] ?? [0]);
103
+ const bestB = Math.max(...scores[b] ?? [0]);
104
+ return bestB - bestA;
105
+ });
106
+ for (const i of order) {
107
+ const row = scores[i];
108
+ let bestJ = -1;
109
+ let bestScore = THRESHOLD;
110
+ for (let j = 0; j < m; j++) {
111
+ if (!usedIncoming.has(j) && (row[j] ?? 0) > bestScore) {
112
+ bestScore = row[j];
113
+ bestJ = j;
114
+ }
115
+ }
116
+ if (bestJ >= 0) {
117
+ matched.set(currentChildren[i], incomingChildren[bestJ]);
118
+ usedIncoming.add(bestJ);
119
+ } else {
120
+ matched.set(currentChildren[i], null);
121
+ }
122
+ }
123
+ return matched;
124
+ }
125
+
3
126
  // src/morpher.ts
4
127
  var DEFAULT_CONFIG = {
5
128
  preserveScroll: true,
@@ -12,23 +135,12 @@ var DOMmorpher = class {
12
135
  }
13
136
  morph(from, to) {
14
137
  const ctx = this.createContext();
138
+ if (!from.__specnav_fp__) {
139
+ stampFingerprints(from);
140
+ }
15
141
  requestAnimationFrame(() => {
16
142
  this.morphElement(from, to, ctx);
17
143
  this.restoreContext(ctx);
18
- this.executeScripts(from);
19
- });
20
- }
21
- executeScripts(element) {
22
- const scripts = element.querySelectorAll("script");
23
- scripts.forEach((oldScript) => {
24
- if (!oldScript.src && oldScript.textContent) {
25
- const newScript = document.createElement("script");
26
- Array.from(oldScript.attributes).forEach((attr) => {
27
- newScript.setAttribute(attr.name, attr.value);
28
- });
29
- newScript.textContent = oldScript.textContent;
30
- oldScript.parentNode?.replaceChild(newScript, oldScript);
31
- }
32
144
  });
33
145
  }
34
146
  createContext() {
@@ -71,6 +183,9 @@ var DOMmorpher = class {
71
183
  });
72
184
  }
73
185
  morphElement(from, to, ctx) {
186
+ if (fingerprintMatches(from, to)) {
187
+ return;
188
+ }
74
189
  this.syncAttributes(from, to);
75
190
  this.morphChildren(from, to, ctx);
76
191
  }
@@ -91,70 +206,32 @@ var DOMmorpher = class {
91
206
  }
92
207
  }
93
208
  morphChildren(from, to, ctx) {
94
- const fromChildren = Array.from(from.childNodes);
95
- const toChildren = Array.from(to.childNodes);
96
- let fromIndex = 0;
97
- let toIndex = 0;
98
- while (toIndex < toChildren.length) {
99
- const toNode = toChildren[toIndex];
100
- const fromNode = fromChildren[fromIndex];
101
- if (!fromNode) {
102
- from.appendChild(toNode.cloneNode(true));
103
- toIndex++;
104
- continue;
209
+ const fromChildren = Array.from(from.children);
210
+ const toChildren = Array.from(to.children);
211
+ if (fromChildren.length === 0 && toChildren.length === 0) {
212
+ if (from.textContent !== to.textContent) {
213
+ from.textContent = to.textContent;
105
214
  }
106
- if (this.nodesMatch(fromNode, toNode)) {
107
- if (fromNode.nodeType === Node.TEXT_NODE) {
108
- if (fromNode.nodeValue !== toNode.nodeValue) {
109
- fromNode.nodeValue = toNode.nodeValue;
110
- }
111
- } else if (fromNode.nodeType === Node.ELEMENT_NODE) {
112
- this.morphElement(fromNode, toNode, ctx);
113
- }
114
- fromIndex++;
115
- toIndex++;
215
+ return;
216
+ }
217
+ const matchMap = matchSiblings(fromChildren, toChildren);
218
+ const processedIncoming = /* @__PURE__ */ new Set();
219
+ for (const [currentChild, incomingChild] of matchMap) {
220
+ if (!incomingChild) {
221
+ from.removeChild(currentChild);
116
222
  } else {
117
- const matchIndex = this.findMatchingNode(
118
- fromChildren,
119
- fromIndex + 1,
120
- toNode
121
- );
122
- if (matchIndex !== -1) {
123
- for (let i = fromIndex; i < matchIndex; i++) {
124
- from.removeChild(fromChildren[i]);
125
- }
126
- fromIndex = matchIndex;
127
- } else {
128
- from.insertBefore(toNode.cloneNode(true), fromNode);
129
- toIndex++;
130
- }
223
+ this.morphElement(currentChild, incomingChild, ctx);
224
+ processedIncoming.add(incomingChild);
131
225
  }
132
226
  }
133
- while (fromIndex < fromChildren.length) {
134
- from.removeChild(fromChildren[fromIndex]);
135
- fromIndex++;
136
- }
137
- }
138
- nodesMatch(a, b) {
139
- if (a.nodeType !== b.nodeType) return false;
140
- if (a.nodeType === Node.ELEMENT_NODE) {
141
- const aEl = a;
142
- const bEl = b;
143
- if (aEl.tagName !== bEl.tagName) return false;
144
- const aId = aEl.getAttribute("id");
145
- const bId = bEl.getAttribute("id");
146
- if (aId && bId) return aId === bId;
147
- return true;
148
- }
149
- return true;
150
- }
151
- findMatchingNode(nodes, startIndex, target) {
152
- for (let i = startIndex; i < nodes.length; i++) {
153
- if (this.nodesMatch(nodes[i], target)) {
154
- return i;
227
+ for (let j = 0; j < toChildren.length; j++) {
228
+ const incomingChild = toChildren[j];
229
+ if (!processedIncoming.has(incomingChild)) {
230
+ const cloned = incomingChild.cloneNode(true);
231
+ const refNode = from.children[j] || null;
232
+ from.insertBefore(cloned, refNode);
155
233
  }
156
234
  }
157
- return -1;
158
235
  }
159
236
  };
160
237
  function createMorpher(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/morpher.ts"],"names":[],"mappings":"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","file":"morpher.js","sourcesContent":["import type { MorphConfig } from \"./types\";\n\nconst DEFAULT_CONFIG: MorphConfig = {\n preserveScroll: true,\n preserveFocus: true,\n};\n\ninterface MorphContext {\n config: MorphConfig;\n focusedElement: Element | null;\n scrollPositions: Map<Element, { top: number; left: number }>;\n}\n\nexport class DOMmorpher {\n private config: MorphConfig;\n\n constructor(config: Partial<MorphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n morph(from: Element, to: Element): void {\n const ctx = this.createContext();\n requestAnimationFrame(() => {\n this.morphElement(from, to, ctx);\n this.restoreContext(ctx);\n this.executeScripts(from);\n });\n }\n\n private executeScripts(element: Element): void {\n const scripts = element.querySelectorAll(\"script\");\n scripts.forEach((oldScript) => {\n // Only execute inline scripts (not external ones with src)\n if (!oldScript.src && oldScript.textContent) {\n const newScript = document.createElement(\"script\");\n \n // Copy attributes\n Array.from(oldScript.attributes).forEach((attr) => {\n newScript.setAttribute(attr.name, attr.value);\n });\n \n // Copy content\n newScript.textContent = oldScript.textContent;\n \n // Replace old script with new one to trigger execution\n oldScript.parentNode?.replaceChild(newScript, oldScript);\n }\n });\n }\n\n private createContext(): MorphContext {\n const ctx: MorphContext = {\n config: this.config,\n focusedElement: null,\n scrollPositions: new Map(),\n };\n\n if (this.config.preserveFocus) {\n ctx.focusedElement = document.activeElement;\n }\n\n if (this.config.preserveScroll) {\n this.captureScrollPositions(document.body, ctx.scrollPositions);\n }\n\n return ctx;\n }\n\n private restoreContext(ctx: MorphContext): void {\n if (ctx.config.preserveFocus && ctx.focusedElement) {\n const element = ctx.focusedElement as HTMLElement;\n if (element.focus && document.body.contains(element)) {\n element.focus();\n }\n }\n\n if (ctx.config.preserveScroll) {\n ctx.scrollPositions.forEach((pos, element) => {\n element.scrollTop = pos.top;\n element.scrollLeft = pos.left;\n });\n }\n }\n\n private captureScrollPositions(\n element: Element,\n positions: Map<Element, { top: number; left: number }>\n ): void {\n if (element.scrollTop > 0 || element.scrollLeft > 0) {\n positions.set(element, {\n top: element.scrollTop,\n left: element.scrollLeft,\n });\n }\n\n Array.from(element.children).forEach((child) => {\n this.captureScrollPositions(child, positions);\n });\n }\n\n private morphElement(from: Element, to: Element, ctx: MorphContext): void {\n // Sync attributes\n this.syncAttributes(from, to);\n\n // Morph children\n this.morphChildren(from, to, ctx);\n }\n\n private syncAttributes(from: Element, to: Element): void {\n const fromAttrs = from.attributes;\n const toAttrs = to.attributes;\n\n // Remove attributes not in target\n for (let i = fromAttrs.length - 1; i >= 0; i--) {\n const attr = fromAttrs[i]!;\n if (!to.hasAttribute(attr.name)) {\n from.removeAttribute(attr.name);\n }\n }\n\n // Add/update attributes from target\n for (let i = 0; i < toAttrs.length; i++) {\n const attr = toAttrs[i]!;\n if (from.getAttribute(attr.name) !== attr.value) {\n from.setAttribute(attr.name, attr.value);\n }\n }\n }\n\n private morphChildren(from: Element, to: Element, ctx: MorphContext): void {\n const fromChildren = Array.from(from.childNodes);\n const toChildren = Array.from(to.childNodes);\n\n let fromIndex = 0;\n let toIndex = 0;\n\n while (toIndex < toChildren.length) {\n const toNode = toChildren[toIndex]!;\n const fromNode = fromChildren[fromIndex];\n\n if (!fromNode) {\n // Append new node\n from.appendChild(toNode.cloneNode(true));\n toIndex++;\n continue;\n }\n\n if (this.nodesMatch(fromNode, toNode)) {\n // Nodes match - recurse or update text\n if (fromNode.nodeType === Node.TEXT_NODE) {\n if (fromNode.nodeValue !== toNode.nodeValue) {\n fromNode.nodeValue = toNode.nodeValue;\n }\n } else if (fromNode.nodeType === Node.ELEMENT_NODE) {\n this.morphElement(fromNode as Element, toNode as Element, ctx);\n }\n fromIndex++;\n toIndex++;\n } else {\n // Try to find matching node ahead\n const matchIndex = this.findMatchingNode(\n fromChildren,\n fromIndex + 1,\n toNode\n );\n\n if (matchIndex !== -1) {\n // Remove nodes before match\n for (let i = fromIndex; i < matchIndex; i++) {\n from.removeChild(fromChildren[i]!);\n }\n fromIndex = matchIndex;\n } else {\n // Insert new node\n from.insertBefore(toNode.cloneNode(true), fromNode);\n toIndex++;\n }\n }\n }\n\n // Remove remaining old nodes\n while (fromIndex < fromChildren.length) {\n from.removeChild(fromChildren[fromIndex]!);\n fromIndex++;\n }\n }\n\n private nodesMatch(a: Node, b: Node): boolean {\n if (a.nodeType !== b.nodeType) return false;\n\n if (a.nodeType === Node.ELEMENT_NODE) {\n const aEl = a as Element;\n const bEl = b as Element;\n\n if (aEl.tagName !== bEl.tagName) return false;\n\n const aId = aEl.getAttribute(\"id\");\n const bId = bEl.getAttribute(\"id\");\n\n if (aId && bId) return aId === bId;\n\n return true;\n }\n\n return true;\n }\n\n private findMatchingNode(\n nodes: Node[],\n startIndex: number,\n target: Node\n ): number {\n for (let i = startIndex; i < nodes.length; i++) {\n if (this.nodesMatch(nodes[i]!, target)) {\n return i;\n }\n }\n return -1;\n }\n}\n\nexport function createMorpher(config?: Partial<MorphConfig>): DOMmorpher {\n return new DOMmorpher(config);\n}\n\nexport function morph(\n from: Element,\n to: Element,\n config?: Partial<MorphConfig>\n): void {\n const morpher = createMorpher(config);\n morpher.morph(from, to);\n}\n"]}
1
+ {"version":3,"sources":["../src/morpher/fingerprint.ts","../src/morpher/identity.ts","../src/morpher/matcher.ts","../src/morpher.ts"],"names":["fnv1a","_"],"mappings":"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","file":"morpher.js","sourcesContent":["type NodeFingerprint = string;\n\nfunction fnv1a(str: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0;\n }\n return hash.toString(36);\n}\n\nfunction fingerprintNode(node: Element): NodeFingerprint {\n const tag = node.tagName.toLowerCase();\n const attrs = Array.from(node.attributes)\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((a) => `${a.name}=${a.value}`)\n .join(\"|\");\n const text = node.childNodes.length === 0 ? node.textContent ?? \"\" : \"\";\n const childHashes = Array.from(node.children).map(fingerprintNode).join(\",\");\n\n return fnv1a(`${tag}[${attrs}]{${text}}(${childHashes})`);\n}\n\nconst FINGERPRINT_KEY = \"__specnav_fp__\";\n\nexport function stampFingerprints(root: Element): void {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n let node = walker.currentNode as Element;\n while (node) {\n (node as any)[FINGERPRINT_KEY] = fingerprintNode(node);\n node = walker.nextNode() as Element;\n }\n}\n\nexport function fingerprintMatches(current: Element, incoming: Element): boolean {\n const currentFp = (current as any)[FINGERPRINT_KEY];\n if (!currentFp) return false;\n return currentFp === fingerprintNode(incoming);\n}\n\nexport { FINGERPRINT_KEY };\n","function fnv1a(str: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0;\n }\n return hash.toString(36);\n}\n\ninterface IdentityVector {\n tagName: string;\n id: string | null;\n key: string | null;\n classSignature: string;\n attrSignature: string;\n textFingerprint: string | null;\n childCount: number;\n childTagSequence: string;\n domPosition: number;\n role: string | null;\n}\n\nexport function buildIdentityVector(node: Element, depth = 0): IdentityVector {\n const classes = Array.from(node.classList).sort().join(\" \");\n const volatileAttrs = new Set([\"style\", \"class\", \"aria-expanded\", \"aria-selected\"]);\n const stableAttrs = Array.from(node.attributes)\n .filter((a) => !volatileAttrs.has(a.name) && a.name !== \"id\")\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((a) => `${a.name}=${a.value}`)\n .join(\"|\");\n\n return {\n tagName: node.tagName.toLowerCase(),\n id: node.id || null,\n key: node.getAttribute(\"data-key\"),\n classSignature: fnv1a(classes),\n attrSignature: fnv1a(stableAttrs),\n textFingerprint:\n node.children.length === 0 ? (node.textContent ?? \"\").slice(0, 64) : null,\n childCount: node.childElementCount,\n childTagSequence: Array.from(node.children)\n .slice(0, 5)\n .map((c) => c.tagName.toLowerCase())\n .join(\">\"),\n domPosition: Array.from(node.parentElement?.children ?? []).indexOf(node),\n role: node.getAttribute(\"role\"),\n };\n}\n\nexport function identityScore(a: IdentityVector, b: IdentityVector): number {\n if (a.tagName !== b.tagName) return 0;\n\n let score = 0;\n let totalWeight = 0;\n\n const checks: Array<[unknown, unknown, number]> = [\n [a.id, b.id, 40],\n [a.key, b.key, 35],\n [a.classSignature, b.classSignature, 15],\n [a.attrSignature, b.attrSignature, 20],\n [a.textFingerprint, b.textFingerprint, 15],\n [a.childCount, b.childCount, 10],\n [a.childTagSequence, b.childTagSequence, 15],\n [a.domPosition, b.domPosition, 5],\n [a.role, b.role, 10],\n ];\n\n for (const [av, bv, weight] of checks) {\n if (av !== null && bv !== null) {\n totalWeight += weight;\n if (av === bv) score += weight;\n }\n }\n\n return totalWeight === 0 ? 0 : score / totalWeight;\n}\n","import { buildIdentityVector, identityScore } from \"./identity\";\n\nexport function matchSiblings(\n currentChildren: Element[],\n incomingChildren: Element[]\n): Map<Element, Element | null> {\n const n = currentChildren.length;\n const m = incomingChildren.length;\n const THRESHOLD = 0.4;\n\n const scores: number[][] = Array.from({ length: n }, (_, i) =>\n Array.from({ length: m }, (_, j) =>\n identityScore(\n buildIdentityVector(currentChildren[i]!),\n buildIdentityVector(incomingChildren[j]!)\n )\n )\n );\n\n const matched = new Map<Element, Element | null>();\n const usedIncoming = new Set<number>();\n\n const order = currentChildren\n .map((_, i) => i)\n .sort((a, b) => {\n const bestA = Math.max(...(scores[a] ?? [0]));\n const bestB = Math.max(...(scores[b] ?? [0]));\n return bestB - bestA;\n });\n\n for (const i of order) {\n const row = scores[i]!;\n let bestJ = -1;\n let bestScore = THRESHOLD;\n\n for (let j = 0; j < m; j++) {\n if (!usedIncoming.has(j) && (row[j] ?? 0) > bestScore) {\n bestScore = row[j]!;\n bestJ = j;\n }\n }\n\n if (bestJ >= 0) {\n matched.set(currentChildren[i]!, incomingChildren[bestJ]!);\n usedIncoming.add(bestJ);\n } else {\n matched.set(currentChildren[i]!, null);\n }\n }\n\n return matched;\n}\n","import type { MorphConfig } from \"./types\";\nimport { stampFingerprints, fingerprintMatches } from \"./morpher/fingerprint\";\nimport { matchSiblings } from \"./morpher/matcher\";\n\nconst DEFAULT_CONFIG: MorphConfig = {\n preserveScroll: true,\n preserveFocus: true,\n};\n\ninterface MorphContext {\n config: MorphConfig;\n focusedElement: Element | null;\n scrollPositions: Map<Element, { top: number; left: number }>;\n}\n\nexport class DOMmorpher {\n private config: MorphConfig;\n\n constructor(config: Partial<MorphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n morph(from: Element, to: Element): void {\n const ctx = this.createContext();\n \n // Stamp fingerprints on first use\n if (!(from as any).__specnav_fp__) {\n stampFingerprints(from);\n }\n \n requestAnimationFrame(() => {\n this.morphElement(from, to, ctx);\n this.restoreContext(ctx);\n });\n }\n\n private createContext(): MorphContext {\n const ctx: MorphContext = {\n config: this.config,\n focusedElement: null,\n scrollPositions: new Map(),\n };\n\n if (this.config.preserveFocus) {\n ctx.focusedElement = document.activeElement;\n }\n\n if (this.config.preserveScroll) {\n this.captureScrollPositions(document.body, ctx.scrollPositions);\n }\n\n return ctx;\n }\n\n private restoreContext(ctx: MorphContext): void {\n if (ctx.config.preserveFocus && ctx.focusedElement) {\n const element = ctx.focusedElement as HTMLElement;\n if (element.focus && document.body.contains(element)) {\n element.focus();\n }\n }\n\n if (ctx.config.preserveScroll) {\n ctx.scrollPositions.forEach((pos, element) => {\n element.scrollTop = pos.top;\n element.scrollLeft = pos.left;\n });\n }\n }\n\n private captureScrollPositions(\n element: Element,\n positions: Map<Element, { top: number; left: number }>\n ): void {\n if (element.scrollTop > 0 || element.scrollLeft > 0) {\n positions.set(element, {\n top: element.scrollTop,\n left: element.scrollLeft,\n });\n }\n\n Array.from(element.children).forEach((child) => {\n this.captureScrollPositions(child, positions);\n });\n }\n\n private morphElement(from: Element, to: Element, ctx: MorphContext): void {\n // O(1) fingerprint check - skip entire subtree if unchanged\n if (fingerprintMatches(from, to)) {\n return;\n }\n\n // Sync attributes\n this.syncAttributes(from, to);\n\n // Morph children using semantic matching\n this.morphChildren(from, to, ctx);\n }\n\n private syncAttributes(from: Element, to: Element): void {\n const fromAttrs = from.attributes;\n const toAttrs = to.attributes;\n\n for (let i = fromAttrs.length - 1; i >= 0; i--) {\n const attr = fromAttrs[i]!;\n if (!to.hasAttribute(attr.name)) {\n from.removeAttribute(attr.name);\n }\n }\n\n for (let i = 0; i < toAttrs.length; i++) {\n const attr = toAttrs[i]!;\n if (from.getAttribute(attr.name) !== attr.value) {\n from.setAttribute(attr.name, attr.value);\n }\n }\n }\n\n private morphChildren(from: Element, to: Element, ctx: MorphContext): void {\n const fromChildren = Array.from(from.children);\n const toChildren = Array.from(to.children);\n\n // Handle text-only nodes\n if (fromChildren.length === 0 && toChildren.length === 0) {\n if (from.textContent !== to.textContent) {\n from.textContent = to.textContent;\n }\n return;\n }\n\n // Use semantic matching instead of positional\n const matchMap = matchSiblings(fromChildren, toChildren);\n\n // Process matched pairs\n const processedIncoming = new Set<Element>();\n \n for (const [currentChild, incomingChild] of matchMap) {\n if (!incomingChild) {\n // Remove unmatched current node\n from.removeChild(currentChild);\n } else {\n // Recurse into matched pair\n this.morphElement(currentChild, incomingChild, ctx);\n processedIncoming.add(incomingChild);\n }\n }\n\n // Insert new incoming nodes that had no match\n for (let j = 0; j < toChildren.length; j++) {\n const incomingChild = toChildren[j]!;\n if (!processedIncoming.has(incomingChild)) {\n const cloned = incomingChild.cloneNode(true) as Element;\n const refNode = from.children[j] || null;\n from.insertBefore(cloned, refNode);\n }\n }\n }\n}\n\nexport function createMorpher(config?: Partial<MorphConfig>): DOMmorpher {\n return new DOMmorpher(config);\n}\n\nexport function morph(\n from: Element,\n to: Element,\n config?: Partial<MorphConfig>\n): void {\n const morpher = createMorpher(config);\n morpher.morph(from, to);\n}\n"]}
package/dist/morpher.mjs CHANGED
@@ -1,3 +1,126 @@
1
+ // src/morpher/fingerprint.ts
2
+ function fnv1a(str) {
3
+ let hash = 2166136261;
4
+ for (let i = 0; i < str.length; i++) {
5
+ hash ^= str.charCodeAt(i);
6
+ hash = hash * 16777619 >>> 0;
7
+ }
8
+ return hash.toString(36);
9
+ }
10
+ function fingerprintNode(node) {
11
+ const tag = node.tagName.toLowerCase();
12
+ const attrs = Array.from(node.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("|");
13
+ const text = node.childNodes.length === 0 ? node.textContent ?? "" : "";
14
+ const childHashes = Array.from(node.children).map(fingerprintNode).join(",");
15
+ return fnv1a(`${tag}[${attrs}]{${text}}(${childHashes})`);
16
+ }
17
+ var FINGERPRINT_KEY = "__specnav_fp__";
18
+ function stampFingerprints(root) {
19
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
20
+ let node = walker.currentNode;
21
+ while (node) {
22
+ node[FINGERPRINT_KEY] = fingerprintNode(node);
23
+ node = walker.nextNode();
24
+ }
25
+ }
26
+ function fingerprintMatches(current, incoming) {
27
+ const currentFp = current[FINGERPRINT_KEY];
28
+ if (!currentFp) return false;
29
+ return currentFp === fingerprintNode(incoming);
30
+ }
31
+
32
+ // src/morpher/identity.ts
33
+ function fnv1a2(str) {
34
+ let hash = 2166136261;
35
+ for (let i = 0; i < str.length; i++) {
36
+ hash ^= str.charCodeAt(i);
37
+ hash = hash * 16777619 >>> 0;
38
+ }
39
+ return hash.toString(36);
40
+ }
41
+ function buildIdentityVector(node, depth = 0) {
42
+ const classes = Array.from(node.classList).sort().join(" ");
43
+ const volatileAttrs = /* @__PURE__ */ new Set(["style", "class", "aria-expanded", "aria-selected"]);
44
+ const stableAttrs = Array.from(node.attributes).filter((a) => !volatileAttrs.has(a.name) && a.name !== "id").sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("|");
45
+ return {
46
+ tagName: node.tagName.toLowerCase(),
47
+ id: node.id || null,
48
+ key: node.getAttribute("data-key"),
49
+ classSignature: fnv1a2(classes),
50
+ attrSignature: fnv1a2(stableAttrs),
51
+ textFingerprint: node.children.length === 0 ? (node.textContent ?? "").slice(0, 64) : null,
52
+ childCount: node.childElementCount,
53
+ childTagSequence: Array.from(node.children).slice(0, 5).map((c) => c.tagName.toLowerCase()).join(">"),
54
+ domPosition: Array.from(node.parentElement?.children ?? []).indexOf(node),
55
+ role: node.getAttribute("role")
56
+ };
57
+ }
58
+ function identityScore(a, b) {
59
+ if (a.tagName !== b.tagName) return 0;
60
+ let score = 0;
61
+ let totalWeight = 0;
62
+ const checks = [
63
+ [a.id, b.id, 40],
64
+ [a.key, b.key, 35],
65
+ [a.classSignature, b.classSignature, 15],
66
+ [a.attrSignature, b.attrSignature, 20],
67
+ [a.textFingerprint, b.textFingerprint, 15],
68
+ [a.childCount, b.childCount, 10],
69
+ [a.childTagSequence, b.childTagSequence, 15],
70
+ [a.domPosition, b.domPosition, 5],
71
+ [a.role, b.role, 10]
72
+ ];
73
+ for (const [av, bv, weight] of checks) {
74
+ if (av !== null && bv !== null) {
75
+ totalWeight += weight;
76
+ if (av === bv) score += weight;
77
+ }
78
+ }
79
+ return totalWeight === 0 ? 0 : score / totalWeight;
80
+ }
81
+
82
+ // src/morpher/matcher.ts
83
+ function matchSiblings(currentChildren, incomingChildren) {
84
+ const n = currentChildren.length;
85
+ const m = incomingChildren.length;
86
+ const THRESHOLD = 0.4;
87
+ const scores = Array.from(
88
+ { length: n },
89
+ (_, i) => Array.from(
90
+ { length: m },
91
+ (_2, j) => identityScore(
92
+ buildIdentityVector(currentChildren[i]),
93
+ buildIdentityVector(incomingChildren[j])
94
+ )
95
+ )
96
+ );
97
+ const matched = /* @__PURE__ */ new Map();
98
+ const usedIncoming = /* @__PURE__ */ new Set();
99
+ const order = currentChildren.map((_, i) => i).sort((a, b) => {
100
+ const bestA = Math.max(...scores[a] ?? [0]);
101
+ const bestB = Math.max(...scores[b] ?? [0]);
102
+ return bestB - bestA;
103
+ });
104
+ for (const i of order) {
105
+ const row = scores[i];
106
+ let bestJ = -1;
107
+ let bestScore = THRESHOLD;
108
+ for (let j = 0; j < m; j++) {
109
+ if (!usedIncoming.has(j) && (row[j] ?? 0) > bestScore) {
110
+ bestScore = row[j];
111
+ bestJ = j;
112
+ }
113
+ }
114
+ if (bestJ >= 0) {
115
+ matched.set(currentChildren[i], incomingChildren[bestJ]);
116
+ usedIncoming.add(bestJ);
117
+ } else {
118
+ matched.set(currentChildren[i], null);
119
+ }
120
+ }
121
+ return matched;
122
+ }
123
+
1
124
  // src/morpher.ts
2
125
  var DEFAULT_CONFIG = {
3
126
  preserveScroll: true,
@@ -10,23 +133,12 @@ var DOMmorpher = class {
10
133
  }
11
134
  morph(from, to) {
12
135
  const ctx = this.createContext();
136
+ if (!from.__specnav_fp__) {
137
+ stampFingerprints(from);
138
+ }
13
139
  requestAnimationFrame(() => {
14
140
  this.morphElement(from, to, ctx);
15
141
  this.restoreContext(ctx);
16
- this.executeScripts(from);
17
- });
18
- }
19
- executeScripts(element) {
20
- const scripts = element.querySelectorAll("script");
21
- scripts.forEach((oldScript) => {
22
- if (!oldScript.src && oldScript.textContent) {
23
- const newScript = document.createElement("script");
24
- Array.from(oldScript.attributes).forEach((attr) => {
25
- newScript.setAttribute(attr.name, attr.value);
26
- });
27
- newScript.textContent = oldScript.textContent;
28
- oldScript.parentNode?.replaceChild(newScript, oldScript);
29
- }
30
142
  });
31
143
  }
32
144
  createContext() {
@@ -69,6 +181,9 @@ var DOMmorpher = class {
69
181
  });
70
182
  }
71
183
  morphElement(from, to, ctx) {
184
+ if (fingerprintMatches(from, to)) {
185
+ return;
186
+ }
72
187
  this.syncAttributes(from, to);
73
188
  this.morphChildren(from, to, ctx);
74
189
  }
@@ -89,70 +204,32 @@ var DOMmorpher = class {
89
204
  }
90
205
  }
91
206
  morphChildren(from, to, ctx) {
92
- const fromChildren = Array.from(from.childNodes);
93
- const toChildren = Array.from(to.childNodes);
94
- let fromIndex = 0;
95
- let toIndex = 0;
96
- while (toIndex < toChildren.length) {
97
- const toNode = toChildren[toIndex];
98
- const fromNode = fromChildren[fromIndex];
99
- if (!fromNode) {
100
- from.appendChild(toNode.cloneNode(true));
101
- toIndex++;
102
- continue;
207
+ const fromChildren = Array.from(from.children);
208
+ const toChildren = Array.from(to.children);
209
+ if (fromChildren.length === 0 && toChildren.length === 0) {
210
+ if (from.textContent !== to.textContent) {
211
+ from.textContent = to.textContent;
103
212
  }
104
- if (this.nodesMatch(fromNode, toNode)) {
105
- if (fromNode.nodeType === Node.TEXT_NODE) {
106
- if (fromNode.nodeValue !== toNode.nodeValue) {
107
- fromNode.nodeValue = toNode.nodeValue;
108
- }
109
- } else if (fromNode.nodeType === Node.ELEMENT_NODE) {
110
- this.morphElement(fromNode, toNode, ctx);
111
- }
112
- fromIndex++;
113
- toIndex++;
213
+ return;
214
+ }
215
+ const matchMap = matchSiblings(fromChildren, toChildren);
216
+ const processedIncoming = /* @__PURE__ */ new Set();
217
+ for (const [currentChild, incomingChild] of matchMap) {
218
+ if (!incomingChild) {
219
+ from.removeChild(currentChild);
114
220
  } else {
115
- const matchIndex = this.findMatchingNode(
116
- fromChildren,
117
- fromIndex + 1,
118
- toNode
119
- );
120
- if (matchIndex !== -1) {
121
- for (let i = fromIndex; i < matchIndex; i++) {
122
- from.removeChild(fromChildren[i]);
123
- }
124
- fromIndex = matchIndex;
125
- } else {
126
- from.insertBefore(toNode.cloneNode(true), fromNode);
127
- toIndex++;
128
- }
221
+ this.morphElement(currentChild, incomingChild, ctx);
222
+ processedIncoming.add(incomingChild);
129
223
  }
130
224
  }
131
- while (fromIndex < fromChildren.length) {
132
- from.removeChild(fromChildren[fromIndex]);
133
- fromIndex++;
134
- }
135
- }
136
- nodesMatch(a, b) {
137
- if (a.nodeType !== b.nodeType) return false;
138
- if (a.nodeType === Node.ELEMENT_NODE) {
139
- const aEl = a;
140
- const bEl = b;
141
- if (aEl.tagName !== bEl.tagName) return false;
142
- const aId = aEl.getAttribute("id");
143
- const bId = bEl.getAttribute("id");
144
- if (aId && bId) return aId === bId;
145
- return true;
146
- }
147
- return true;
148
- }
149
- findMatchingNode(nodes, startIndex, target) {
150
- for (let i = startIndex; i < nodes.length; i++) {
151
- if (this.nodesMatch(nodes[i], target)) {
152
- return i;
225
+ for (let j = 0; j < toChildren.length; j++) {
226
+ const incomingChild = toChildren[j];
227
+ if (!processedIncoming.has(incomingChild)) {
228
+ const cloned = incomingChild.cloneNode(true);
229
+ const refNode = from.children[j] || null;
230
+ from.insertBefore(cloned, refNode);
153
231
  }
154
232
  }
155
- return -1;
156
233
  }
157
234
  };
158
235
  function createMorpher(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/morpher.ts"],"names":[],"mappings":"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","file":"morpher.mjs","sourcesContent":["import type { MorphConfig } from \"./types\";\n\nconst DEFAULT_CONFIG: MorphConfig = {\n preserveScroll: true,\n preserveFocus: true,\n};\n\ninterface MorphContext {\n config: MorphConfig;\n focusedElement: Element | null;\n scrollPositions: Map<Element, { top: number; left: number }>;\n}\n\nexport class DOMmorpher {\n private config: MorphConfig;\n\n constructor(config: Partial<MorphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n morph(from: Element, to: Element): void {\n const ctx = this.createContext();\n requestAnimationFrame(() => {\n this.morphElement(from, to, ctx);\n this.restoreContext(ctx);\n this.executeScripts(from);\n });\n }\n\n private executeScripts(element: Element): void {\n const scripts = element.querySelectorAll(\"script\");\n scripts.forEach((oldScript) => {\n // Only execute inline scripts (not external ones with src)\n if (!oldScript.src && oldScript.textContent) {\n const newScript = document.createElement(\"script\");\n \n // Copy attributes\n Array.from(oldScript.attributes).forEach((attr) => {\n newScript.setAttribute(attr.name, attr.value);\n });\n \n // Copy content\n newScript.textContent = oldScript.textContent;\n \n // Replace old script with new one to trigger execution\n oldScript.parentNode?.replaceChild(newScript, oldScript);\n }\n });\n }\n\n private createContext(): MorphContext {\n const ctx: MorphContext = {\n config: this.config,\n focusedElement: null,\n scrollPositions: new Map(),\n };\n\n if (this.config.preserveFocus) {\n ctx.focusedElement = document.activeElement;\n }\n\n if (this.config.preserveScroll) {\n this.captureScrollPositions(document.body, ctx.scrollPositions);\n }\n\n return ctx;\n }\n\n private restoreContext(ctx: MorphContext): void {\n if (ctx.config.preserveFocus && ctx.focusedElement) {\n const element = ctx.focusedElement as HTMLElement;\n if (element.focus && document.body.contains(element)) {\n element.focus();\n }\n }\n\n if (ctx.config.preserveScroll) {\n ctx.scrollPositions.forEach((pos, element) => {\n element.scrollTop = pos.top;\n element.scrollLeft = pos.left;\n });\n }\n }\n\n private captureScrollPositions(\n element: Element,\n positions: Map<Element, { top: number; left: number }>\n ): void {\n if (element.scrollTop > 0 || element.scrollLeft > 0) {\n positions.set(element, {\n top: element.scrollTop,\n left: element.scrollLeft,\n });\n }\n\n Array.from(element.children).forEach((child) => {\n this.captureScrollPositions(child, positions);\n });\n }\n\n private morphElement(from: Element, to: Element, ctx: MorphContext): void {\n // Sync attributes\n this.syncAttributes(from, to);\n\n // Morph children\n this.morphChildren(from, to, ctx);\n }\n\n private syncAttributes(from: Element, to: Element): void {\n const fromAttrs = from.attributes;\n const toAttrs = to.attributes;\n\n // Remove attributes not in target\n for (let i = fromAttrs.length - 1; i >= 0; i--) {\n const attr = fromAttrs[i]!;\n if (!to.hasAttribute(attr.name)) {\n from.removeAttribute(attr.name);\n }\n }\n\n // Add/update attributes from target\n for (let i = 0; i < toAttrs.length; i++) {\n const attr = toAttrs[i]!;\n if (from.getAttribute(attr.name) !== attr.value) {\n from.setAttribute(attr.name, attr.value);\n }\n }\n }\n\n private morphChildren(from: Element, to: Element, ctx: MorphContext): void {\n const fromChildren = Array.from(from.childNodes);\n const toChildren = Array.from(to.childNodes);\n\n let fromIndex = 0;\n let toIndex = 0;\n\n while (toIndex < toChildren.length) {\n const toNode = toChildren[toIndex]!;\n const fromNode = fromChildren[fromIndex];\n\n if (!fromNode) {\n // Append new node\n from.appendChild(toNode.cloneNode(true));\n toIndex++;\n continue;\n }\n\n if (this.nodesMatch(fromNode, toNode)) {\n // Nodes match - recurse or update text\n if (fromNode.nodeType === Node.TEXT_NODE) {\n if (fromNode.nodeValue !== toNode.nodeValue) {\n fromNode.nodeValue = toNode.nodeValue;\n }\n } else if (fromNode.nodeType === Node.ELEMENT_NODE) {\n this.morphElement(fromNode as Element, toNode as Element, ctx);\n }\n fromIndex++;\n toIndex++;\n } else {\n // Try to find matching node ahead\n const matchIndex = this.findMatchingNode(\n fromChildren,\n fromIndex + 1,\n toNode\n );\n\n if (matchIndex !== -1) {\n // Remove nodes before match\n for (let i = fromIndex; i < matchIndex; i++) {\n from.removeChild(fromChildren[i]!);\n }\n fromIndex = matchIndex;\n } else {\n // Insert new node\n from.insertBefore(toNode.cloneNode(true), fromNode);\n toIndex++;\n }\n }\n }\n\n // Remove remaining old nodes\n while (fromIndex < fromChildren.length) {\n from.removeChild(fromChildren[fromIndex]!);\n fromIndex++;\n }\n }\n\n private nodesMatch(a: Node, b: Node): boolean {\n if (a.nodeType !== b.nodeType) return false;\n\n if (a.nodeType === Node.ELEMENT_NODE) {\n const aEl = a as Element;\n const bEl = b as Element;\n\n if (aEl.tagName !== bEl.tagName) return false;\n\n const aId = aEl.getAttribute(\"id\");\n const bId = bEl.getAttribute(\"id\");\n\n if (aId && bId) return aId === bId;\n\n return true;\n }\n\n return true;\n }\n\n private findMatchingNode(\n nodes: Node[],\n startIndex: number,\n target: Node\n ): number {\n for (let i = startIndex; i < nodes.length; i++) {\n if (this.nodesMatch(nodes[i]!, target)) {\n return i;\n }\n }\n return -1;\n }\n}\n\nexport function createMorpher(config?: Partial<MorphConfig>): DOMmorpher {\n return new DOMmorpher(config);\n}\n\nexport function morph(\n from: Element,\n to: Element,\n config?: Partial<MorphConfig>\n): void {\n const morpher = createMorpher(config);\n morpher.morph(from, to);\n}\n"]}
1
+ {"version":3,"sources":["../src/morpher/fingerprint.ts","../src/morpher/identity.ts","../src/morpher/matcher.ts","../src/morpher.ts"],"names":["fnv1a","_"],"mappings":";AAEA,SAAS,MAAM,GAAA,EAAqB;AAClC,EAAA,IAAI,IAAA,GAAO,UAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,IAAA,IAAQ,GAAA,CAAI,WAAW,CAAC,CAAA;AACxB,IAAA,IAAA,GAAQ,OAAO,QAAA,KAAgB,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,IAAA,CAAK,SAAS,EAAE,CAAA;AACzB;AAEA,SAAS,gBAAgB,IAAA,EAAgC;AACvD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAY;AACrC,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,UAAU,CAAA,CACrC,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,IAAA,CAAK,aAAA,CAAc,CAAA,CAAE,IAAI,CAAC,CAAA,CAC3C,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAG,CAAA,CAAE,IAAI,CAAA,CAAA,EAAI,CAAA,CAAE,KAAK,CAAA,CAAE,CAAA,CACjC,KAAK,GAAG,CAAA;AACX,EAAA,MAAM,OAAO,IAAA,CAAK,UAAA,CAAW,WAAW,CAAA,GAAI,IAAA,CAAK,eAAe,EAAA,GAAK,EAAA;AACrE,EAAA,MAAM,WAAA,GAAc,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAQ,EAAE,GAAA,CAAI,eAAe,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAE3E,EAAA,OAAO,KAAA,CAAM,GAAG,GAAG,CAAA,CAAA,EAAI,KAAK,CAAA,EAAA,EAAK,IAAI,CAAA,EAAA,EAAK,WAAW,CAAA,CAAA,CAAG,CAAA;AAC1D;AAEA,IAAM,eAAA,GAAkB,gBAAA;AAEjB,SAAS,kBAAkB,IAAA,EAAqB;AACrD,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,gBAAA,CAAiB,IAAA,EAAM,WAAW,YAAY,CAAA;AACtE,EAAA,IAAI,OAAO,MAAA,CAAO,WAAA;AAClB,EAAA,OAAO,IAAA,EAAM;AACX,IAAC,IAAA,CAAa,eAAe,CAAA,GAAI,eAAA,CAAgB,IAAI,CAAA;AACrD,IAAA,IAAA,GAAO,OAAO,QAAA,EAAS;AAAA,EACzB;AACF;AAEO,SAAS,kBAAA,CAAmB,SAAkB,QAAA,EAA4B;AAC/E,EAAA,MAAM,SAAA,GAAa,QAAgB,eAAe,CAAA;AAClD,EAAA,IAAI,CAAC,WAAW,OAAO,KAAA;AACvB,EAAA,OAAO,SAAA,KAAc,gBAAgB,QAAQ,CAAA;AAC/C;;;ACtCA,SAASA,OAAM,GAAA,EAAqB;AAClC,EAAA,IAAI,IAAA,GAAO,UAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,IAAA,IAAQ,GAAA,CAAI,WAAW,CAAC,CAAA;AACxB,IAAA,IAAA,GAAQ,OAAO,QAAA,KAAgB,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,IAAA,CAAK,SAAS,EAAE,CAAA;AACzB;AAeO,SAAS,mBAAA,CAAoB,IAAA,EAAe,KAAA,GAAQ,CAAA,EAAmB;AAC5E,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,IAAA,CAAK,SAAS,CAAA,CAAE,IAAA,EAAK,CAAE,IAAA,CAAK,GAAG,CAAA;AAC1D,EAAA,MAAM,aAAA,uBAAoB,GAAA,CAAI,CAAC,SAAS,OAAA,EAAS,eAAA,EAAiB,eAAe,CAAC,CAAA;AAClF,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA,CAAK,KAAK,UAAU,CAAA,CAC3C,OAAO,CAAC,CAAA,KAAM,CAAC,aAAA,CAAc,GAAA,CAAI,EAAE,IAAI,CAAA,IAAK,EAAE,IAAA,KAAS,IAAI,EAC3D,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,EAAE,IAAA,CAAK,aAAA,CAAc,EAAE,IAAI,CAAC,EAC3C,GAAA,CAAI,CAAC,MAAM,CAAA,EAAG,CAAA,CAAE,IAAI,CAAA,CAAA,EAAI,CAAA,CAAE,KAAK,CAAA,CAAE,CAAA,CACjC,KAAK,GAAG,CAAA;AAEX,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAY;AAAA,IAClC,EAAA,EAAI,KAAK,EAAA,IAAM,IAAA;AAAA,IACf,GAAA,EAAK,IAAA,CAAK,YAAA,CAAa,UAAU,CAAA;AAAA,IACjC,cAAA,EAAgBA,OAAM,OAAO,CAAA;AAAA,IAC7B,aAAA,EAAeA,OAAM,WAAW,CAAA;AAAA,IAChC,eAAA,EACE,IAAA,CAAK,QAAA,CAAS,MAAA,KAAW,CAAA,GAAA,CAAK,IAAA,CAAK,WAAA,IAAe,EAAA,EAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,IAAA;AAAA,IACvE,YAAY,IAAA,CAAK,iBAAA;AAAA,IACjB,kBAAkB,KAAA,CAAM,IAAA,CAAK,KAAK,QAAQ,CAAA,CACvC,MAAM,CAAA,EAAG,CAAC,EACV,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,OAAA,CAAQ,aAAa,CAAA,CAClC,KAAK,GAAG,CAAA;AAAA,IACX,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,aAAA,EAAe,YAAY,EAAE,CAAA,CAAE,OAAA,CAAQ,IAAI,CAAA;AAAA,IACxE,IAAA,EAAM,IAAA,CAAK,YAAA,CAAa,MAAM;AAAA,GAChC;AACF;AAEO,SAAS,aAAA,CAAc,GAAmB,CAAA,EAA2B;AAC1E,EAAA,IAAI,CAAA,CAAE,OAAA,KAAY,CAAA,CAAE,OAAA,EAAS,OAAO,CAAA;AAEpC,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,IAAI,WAAA,GAAc,CAAA;AAElB,EAAA,MAAM,MAAA,GAA4C;AAAA,IAChD,CAAC,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,IAAI,EAAE,CAAA;AAAA,IACf,CAAC,CAAA,CAAE,GAAA,EAAK,CAAA,CAAE,KAAK,EAAE,CAAA;AAAA,IACjB,CAAC,CAAA,CAAE,cAAA,EAAgB,CAAA,CAAE,gBAAgB,EAAE,CAAA;AAAA,IACvC,CAAC,CAAA,CAAE,aAAA,EAAe,CAAA,CAAE,eAAe,EAAE,CAAA;AAAA,IACrC,CAAC,CAAA,CAAE,eAAA,EAAiB,CAAA,CAAE,iBAAiB,EAAE,CAAA;AAAA,IACzC,CAAC,CAAA,CAAE,UAAA,EAAY,CAAA,CAAE,YAAY,EAAE,CAAA;AAAA,IAC/B,CAAC,CAAA,CAAE,gBAAA,EAAkB,CAAA,CAAE,kBAAkB,EAAE,CAAA;AAAA,IAC3C,CAAC,CAAA,CAAE,WAAA,EAAa,CAAA,CAAE,aAAa,CAAC,CAAA;AAAA,IAChC,CAAC,CAAA,CAAE,IAAA,EAAM,CAAA,CAAE,MAAM,EAAE;AAAA,GACrB;AAEA,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,EAAA,EAAI,MAAM,KAAK,MAAA,EAAQ;AACrC,IAAA,IAAI,EAAA,KAAO,IAAA,IAAQ,EAAA,KAAO,IAAA,EAAM;AAC9B,MAAA,WAAA,IAAe,MAAA;AACf,MAAA,IAAI,EAAA,KAAO,IAAI,KAAA,IAAS,MAAA;AAAA,IAC1B;AAAA,EACF;AAEA,EAAA,OAAO,WAAA,KAAgB,CAAA,GAAI,CAAA,GAAI,KAAA,GAAQ,WAAA;AACzC;;;ACzEO,SAAS,aAAA,CACd,iBACA,gBAAA,EAC8B;AAC9B,EAAA,MAAM,IAAI,eAAA,CAAgB,MAAA;AAC1B,EAAA,MAAM,IAAI,gBAAA,CAAiB,MAAA;AAC3B,EAAA,MAAM,SAAA,GAAY,GAAA;AAElB,EAAA,MAAM,SAAqB,KAAA,CAAM,IAAA;AAAA,IAAK,EAAE,QAAQ,CAAA,EAAE;AAAA,IAAG,CAAC,CAAA,EAAG,CAAA,KACvD,KAAA,CAAM,IAAA;AAAA,MAAK,EAAE,QAAQ,CAAA,EAAE;AAAA,MAAG,CAACC,IAAG,CAAA,KAC5B,aAAA;AAAA,QACE,mBAAA,CAAoB,eAAA,CAAgB,CAAC,CAAE,CAAA;AAAA,QACvC,mBAAA,CAAoB,gBAAA,CAAiB,CAAC,CAAE;AAAA;AAC1C;AACF,GACF;AAEA,EAAA,MAAM,OAAA,uBAAc,GAAA,EAA6B;AACjD,EAAA,MAAM,YAAA,uBAAmB,GAAA,EAAY;AAErC,EAAA,MAAM,KAAA,GAAQ,eAAA,CACX,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,KAAM,CAAC,CAAA,CACf,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACd,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,GAAI,OAAO,CAAC,CAAA,IAAK,CAAC,CAAC,CAAE,CAAA;AAC5C,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,GAAI,OAAO,CAAC,CAAA,IAAK,CAAC,CAAC,CAAE,CAAA;AAC5C,IAAA,OAAO,KAAA,GAAQ,KAAA;AAAA,EACjB,CAAC,CAAA;AAEH,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,MAAM,GAAA,GAAM,OAAO,CAAC,CAAA;AACpB,IAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,IAAA,IAAI,SAAA,GAAY,SAAA;AAEhB,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,MAAA,IAAI,CAAC,aAAa,GAAA,CAAI,CAAC,MAAM,GAAA,CAAI,CAAC,CAAA,IAAK,CAAA,IAAK,SAAA,EAAW;AACrD,QAAA,SAAA,GAAY,IAAI,CAAC,CAAA;AACjB,QAAA,KAAA,GAAQ,CAAA;AAAA,MACV;AAAA,IACF;AAEA,IAAA,IAAI,SAAS,CAAA,EAAG;AACd,MAAA,OAAA,CAAQ,IAAI,eAAA,CAAgB,CAAC,CAAA,EAAI,gBAAA,CAAiB,KAAK,CAAE,CAAA;AACzD,MAAA,YAAA,CAAa,IAAI,KAAK,CAAA;AAAA,IACxB,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,GAAA,CAAI,eAAA,CAAgB,CAAC,CAAA,EAAI,IAAI,CAAA;AAAA,IACvC;AAAA,EACF;AAEA,EAAA,OAAO,OAAA;AACT;;;AC/CA,IAAM,cAAA,GAA8B;AAAA,EAClC,cAAA,EAAgB,IAAA;AAAA,EAChB,aAAA,EAAe;AACjB,CAAA;AAQO,IAAM,aAAN,MAAiB;AAAA,EACd,MAAA;AAAA,EAER,WAAA,CAAY,MAAA,GAA+B,EAAC,EAAG;AAC7C,IAAA,IAAA,CAAK,MAAA,GAAS,EAAE,GAAG,cAAA,EAAgB,GAAG,MAAA,EAAO;AAAA,EAC/C;AAAA,EAEA,KAAA,CAAM,MAAe,EAAA,EAAmB;AACtC,IAAA,MAAM,GAAA,GAAM,KAAK,aAAA,EAAc;AAG/B,IAAA,IAAI,CAAE,KAAa,cAAA,EAAgB;AACjC,MAAA,iBAAA,CAAkB,IAAI,CAAA;AAAA,IACxB;AAEA,IAAA,qBAAA,CAAsB,MAAM;AAC1B,MAAA,IAAA,CAAK,YAAA,CAAa,IAAA,EAAM,EAAA,EAAI,GAAG,CAAA;AAC/B,MAAA,IAAA,CAAK,eAAe,GAAG,CAAA;AAAA,IACzB,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,aAAA,GAA8B;AACpC,IAAA,MAAM,GAAA,GAAoB;AAAA,MACxB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,cAAA,EAAgB,IAAA;AAAA,MAChB,eAAA,sBAAqB,GAAA;AAAI,KAC3B;AAEA,IAAA,IAAI,IAAA,CAAK,OAAO,aAAA,EAAe;AAC7B,MAAA,GAAA,CAAI,iBAAiB,QAAA,CAAS,aAAA;AAAA,IAChC;AAEA,IAAA,IAAI,IAAA,CAAK,OAAO,cAAA,EAAgB;AAC9B,MAAA,IAAA,CAAK,sBAAA,CAAuB,QAAA,CAAS,IAAA,EAAM,GAAA,CAAI,eAAe,CAAA;AAAA,IAChE;AAEA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA,EAEQ,eAAe,GAAA,EAAyB;AAC9C,IAAA,IAAI,GAAA,CAAI,MAAA,CAAO,aAAA,IAAiB,GAAA,CAAI,cAAA,EAAgB;AAClD,MAAA,MAAM,UAAU,GAAA,CAAI,cAAA;AACpB,MAAA,IAAI,QAAQ,KAAA,IAAS,QAAA,CAAS,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA,EAAG;AACpD,QAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,MAChB;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,CAAI,OAAO,cAAA,EAAgB;AAC7B,MAAA,GAAA,CAAI,eAAA,CAAgB,OAAA,CAAQ,CAAC,GAAA,EAAK,OAAA,KAAY;AAC5C,QAAA,OAAA,CAAQ,YAAY,GAAA,CAAI,GAAA;AACxB,QAAA,OAAA,CAAQ,aAAa,GAAA,CAAI,IAAA;AAAA,MAC3B,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEQ,sBAAA,CACN,SACA,SAAA,EACM;AACN,IAAA,IAAI,OAAA,CAAQ,SAAA,GAAY,CAAA,IAAK,OAAA,CAAQ,aAAa,CAAA,EAAG;AACnD,MAAA,SAAA,CAAU,IAAI,OAAA,EAAS;AAAA,QACrB,KAAK,OAAA,CAAQ,SAAA;AAAA,QACb,MAAM,OAAA,CAAQ;AAAA,OACf,CAAA;AAAA,IACH;AAEA,IAAA,KAAA,CAAM,KAAK,OAAA,CAAQ,QAAQ,CAAA,CAAE,OAAA,CAAQ,CAAC,KAAA,KAAU;AAC9C,MAAA,IAAA,CAAK,sBAAA,CAAuB,OAAO,SAAS,CAAA;AAAA,IAC9C,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,YAAA,CAAa,IAAA,EAAe,EAAA,EAAa,GAAA,EAAyB;AAExE,IAAA,IAAI,kBAAA,CAAmB,IAAA,EAAM,EAAE,CAAA,EAAG;AAChC,MAAA;AAAA,IACF;AAGA,IAAA,IAAA,CAAK,cAAA,CAAe,MAAM,EAAE,CAAA;AAG5B,IAAA,IAAA,CAAK,aAAA,CAAc,IAAA,EAAM,EAAA,EAAI,GAAG,CAAA;AAAA,EAClC;AAAA,EAEQ,cAAA,CAAe,MAAe,EAAA,EAAmB;AACvD,IAAA,MAAM,YAAY,IAAA,CAAK,UAAA;AACvB,IAAA,MAAM,UAAU,EAAA,CAAG,UAAA;AAEnB,IAAA,KAAA,IAAS,IAAI,SAAA,CAAU,MAAA,GAAS,CAAA,EAAG,CAAA,IAAK,GAAG,CAAA,EAAA,EAAK;AAC9C,MAAA,MAAM,IAAA,GAAO,UAAU,CAAC,CAAA;AACxB,MAAA,IAAI,CAAC,EAAA,CAAG,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA,EAAG;AAC/B,QAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,IAAI,CAAA;AAAA,MAChC;AAAA,IACF;AAEA,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,OAAA,CAAQ,QAAQ,CAAA,EAAA,EAAK;AACvC,MAAA,MAAM,IAAA,GAAO,QAAQ,CAAC,CAAA;AACtB,MAAA,IAAI,KAAK,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA,KAAM,KAAK,KAAA,EAAO;AAC/C,QAAA,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,IAAA,EAAM,IAAA,CAAK,KAAK,CAAA;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAA,CAAc,IAAA,EAAe,EAAA,EAAa,GAAA,EAAyB;AACzE,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAQ,CAAA;AAC7C,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,IAAA,CAAK,EAAA,CAAG,QAAQ,CAAA;AAGzC,IAAA,IAAI,YAAA,CAAa,MAAA,KAAW,CAAA,IAAK,UAAA,CAAW,WAAW,CAAA,EAAG;AACxD,MAAA,IAAI,IAAA,CAAK,WAAA,KAAgB,EAAA,CAAG,WAAA,EAAa;AACvC,QAAA,IAAA,CAAK,cAAc,EAAA,CAAG,WAAA;AAAA,MACxB;AACA,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,QAAA,GAAW,aAAA,CAAc,YAAA,EAAc,UAAU,CAAA;AAGvD,IAAA,MAAM,iBAAA,uBAAwB,GAAA,EAAa;AAE3C,IAAA,KAAA,MAAW,CAAC,YAAA,EAAc,aAAa,CAAA,IAAK,QAAA,EAAU;AACpD,MAAA,IAAI,CAAC,aAAA,EAAe;AAElB,QAAA,IAAA,CAAK,YAAY,YAAY,CAAA;AAAA,MAC/B,CAAA,MAAO;AAEL,QAAA,IAAA,CAAK,YAAA,CAAa,YAAA,EAAc,aAAA,EAAe,GAAG,CAAA;AAClD,QAAA,iBAAA,CAAkB,IAAI,aAAa,CAAA;AAAA,MACrC;AAAA,IACF;AAGA,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,UAAA,CAAW,QAAQ,CAAA,EAAA,EAAK;AAC1C,MAAA,MAAM,aAAA,GAAgB,WAAW,CAAC,CAAA;AAClC,MAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,aAAa,CAAA,EAAG;AACzC,QAAA,MAAM,MAAA,GAAS,aAAA,CAAc,SAAA,CAAU,IAAI,CAAA;AAC3C,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,CAAC,CAAA,IAAK,IAAA;AACpC,QAAA,IAAA,CAAK,YAAA,CAAa,QAAQ,OAAO,CAAA;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,cAAc,MAAA,EAA2C;AACvE,EAAA,OAAO,IAAI,WAAW,MAAM,CAAA;AAC9B;AAEO,SAAS,KAAA,CACd,IAAA,EACA,EAAA,EACA,MAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,cAAc,MAAM,CAAA;AACpC,EAAA,OAAA,CAAQ,KAAA,CAAM,MAAM,EAAE,CAAA;AACxB","file":"morpher.mjs","sourcesContent":["type NodeFingerprint = string;\n\nfunction fnv1a(str: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0;\n }\n return hash.toString(36);\n}\n\nfunction fingerprintNode(node: Element): NodeFingerprint {\n const tag = node.tagName.toLowerCase();\n const attrs = Array.from(node.attributes)\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((a) => `${a.name}=${a.value}`)\n .join(\"|\");\n const text = node.childNodes.length === 0 ? node.textContent ?? \"\" : \"\";\n const childHashes = Array.from(node.children).map(fingerprintNode).join(\",\");\n\n return fnv1a(`${tag}[${attrs}]{${text}}(${childHashes})`);\n}\n\nconst FINGERPRINT_KEY = \"__specnav_fp__\";\n\nexport function stampFingerprints(root: Element): void {\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n let node = walker.currentNode as Element;\n while (node) {\n (node as any)[FINGERPRINT_KEY] = fingerprintNode(node);\n node = walker.nextNode() as Element;\n }\n}\n\nexport function fingerprintMatches(current: Element, incoming: Element): boolean {\n const currentFp = (current as any)[FINGERPRINT_KEY];\n if (!currentFp) return false;\n return currentFp === fingerprintNode(incoming);\n}\n\nexport { FINGERPRINT_KEY };\n","function fnv1a(str: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0;\n }\n return hash.toString(36);\n}\n\ninterface IdentityVector {\n tagName: string;\n id: string | null;\n key: string | null;\n classSignature: string;\n attrSignature: string;\n textFingerprint: string | null;\n childCount: number;\n childTagSequence: string;\n domPosition: number;\n role: string | null;\n}\n\nexport function buildIdentityVector(node: Element, depth = 0): IdentityVector {\n const classes = Array.from(node.classList).sort().join(\" \");\n const volatileAttrs = new Set([\"style\", \"class\", \"aria-expanded\", \"aria-selected\"]);\n const stableAttrs = Array.from(node.attributes)\n .filter((a) => !volatileAttrs.has(a.name) && a.name !== \"id\")\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((a) => `${a.name}=${a.value}`)\n .join(\"|\");\n\n return {\n tagName: node.tagName.toLowerCase(),\n id: node.id || null,\n key: node.getAttribute(\"data-key\"),\n classSignature: fnv1a(classes),\n attrSignature: fnv1a(stableAttrs),\n textFingerprint:\n node.children.length === 0 ? (node.textContent ?? \"\").slice(0, 64) : null,\n childCount: node.childElementCount,\n childTagSequence: Array.from(node.children)\n .slice(0, 5)\n .map((c) => c.tagName.toLowerCase())\n .join(\">\"),\n domPosition: Array.from(node.parentElement?.children ?? []).indexOf(node),\n role: node.getAttribute(\"role\"),\n };\n}\n\nexport function identityScore(a: IdentityVector, b: IdentityVector): number {\n if (a.tagName !== b.tagName) return 0;\n\n let score = 0;\n let totalWeight = 0;\n\n const checks: Array<[unknown, unknown, number]> = [\n [a.id, b.id, 40],\n [a.key, b.key, 35],\n [a.classSignature, b.classSignature, 15],\n [a.attrSignature, b.attrSignature, 20],\n [a.textFingerprint, b.textFingerprint, 15],\n [a.childCount, b.childCount, 10],\n [a.childTagSequence, b.childTagSequence, 15],\n [a.domPosition, b.domPosition, 5],\n [a.role, b.role, 10],\n ];\n\n for (const [av, bv, weight] of checks) {\n if (av !== null && bv !== null) {\n totalWeight += weight;\n if (av === bv) score += weight;\n }\n }\n\n return totalWeight === 0 ? 0 : score / totalWeight;\n}\n","import { buildIdentityVector, identityScore } from \"./identity\";\n\nexport function matchSiblings(\n currentChildren: Element[],\n incomingChildren: Element[]\n): Map<Element, Element | null> {\n const n = currentChildren.length;\n const m = incomingChildren.length;\n const THRESHOLD = 0.4;\n\n const scores: number[][] = Array.from({ length: n }, (_, i) =>\n Array.from({ length: m }, (_, j) =>\n identityScore(\n buildIdentityVector(currentChildren[i]!),\n buildIdentityVector(incomingChildren[j]!)\n )\n )\n );\n\n const matched = new Map<Element, Element | null>();\n const usedIncoming = new Set<number>();\n\n const order = currentChildren\n .map((_, i) => i)\n .sort((a, b) => {\n const bestA = Math.max(...(scores[a] ?? [0]));\n const bestB = Math.max(...(scores[b] ?? [0]));\n return bestB - bestA;\n });\n\n for (const i of order) {\n const row = scores[i]!;\n let bestJ = -1;\n let bestScore = THRESHOLD;\n\n for (let j = 0; j < m; j++) {\n if (!usedIncoming.has(j) && (row[j] ?? 0) > bestScore) {\n bestScore = row[j]!;\n bestJ = j;\n }\n }\n\n if (bestJ >= 0) {\n matched.set(currentChildren[i]!, incomingChildren[bestJ]!);\n usedIncoming.add(bestJ);\n } else {\n matched.set(currentChildren[i]!, null);\n }\n }\n\n return matched;\n}\n","import type { MorphConfig } from \"./types\";\nimport { stampFingerprints, fingerprintMatches } from \"./morpher/fingerprint\";\nimport { matchSiblings } from \"./morpher/matcher\";\n\nconst DEFAULT_CONFIG: MorphConfig = {\n preserveScroll: true,\n preserveFocus: true,\n};\n\ninterface MorphContext {\n config: MorphConfig;\n focusedElement: Element | null;\n scrollPositions: Map<Element, { top: number; left: number }>;\n}\n\nexport class DOMmorpher {\n private config: MorphConfig;\n\n constructor(config: Partial<MorphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n morph(from: Element, to: Element): void {\n const ctx = this.createContext();\n \n // Stamp fingerprints on first use\n if (!(from as any).__specnav_fp__) {\n stampFingerprints(from);\n }\n \n requestAnimationFrame(() => {\n this.morphElement(from, to, ctx);\n this.restoreContext(ctx);\n });\n }\n\n private createContext(): MorphContext {\n const ctx: MorphContext = {\n config: this.config,\n focusedElement: null,\n scrollPositions: new Map(),\n };\n\n if (this.config.preserveFocus) {\n ctx.focusedElement = document.activeElement;\n }\n\n if (this.config.preserveScroll) {\n this.captureScrollPositions(document.body, ctx.scrollPositions);\n }\n\n return ctx;\n }\n\n private restoreContext(ctx: MorphContext): void {\n if (ctx.config.preserveFocus && ctx.focusedElement) {\n const element = ctx.focusedElement as HTMLElement;\n if (element.focus && document.body.contains(element)) {\n element.focus();\n }\n }\n\n if (ctx.config.preserveScroll) {\n ctx.scrollPositions.forEach((pos, element) => {\n element.scrollTop = pos.top;\n element.scrollLeft = pos.left;\n });\n }\n }\n\n private captureScrollPositions(\n element: Element,\n positions: Map<Element, { top: number; left: number }>\n ): void {\n if (element.scrollTop > 0 || element.scrollLeft > 0) {\n positions.set(element, {\n top: element.scrollTop,\n left: element.scrollLeft,\n });\n }\n\n Array.from(element.children).forEach((child) => {\n this.captureScrollPositions(child, positions);\n });\n }\n\n private morphElement(from: Element, to: Element, ctx: MorphContext): void {\n // O(1) fingerprint check - skip entire subtree if unchanged\n if (fingerprintMatches(from, to)) {\n return;\n }\n\n // Sync attributes\n this.syncAttributes(from, to);\n\n // Morph children using semantic matching\n this.morphChildren(from, to, ctx);\n }\n\n private syncAttributes(from: Element, to: Element): void {\n const fromAttrs = from.attributes;\n const toAttrs = to.attributes;\n\n for (let i = fromAttrs.length - 1; i >= 0; i--) {\n const attr = fromAttrs[i]!;\n if (!to.hasAttribute(attr.name)) {\n from.removeAttribute(attr.name);\n }\n }\n\n for (let i = 0; i < toAttrs.length; i++) {\n const attr = toAttrs[i]!;\n if (from.getAttribute(attr.name) !== attr.value) {\n from.setAttribute(attr.name, attr.value);\n }\n }\n }\n\n private morphChildren(from: Element, to: Element, ctx: MorphContext): void {\n const fromChildren = Array.from(from.children);\n const toChildren = Array.from(to.children);\n\n // Handle text-only nodes\n if (fromChildren.length === 0 && toChildren.length === 0) {\n if (from.textContent !== to.textContent) {\n from.textContent = to.textContent;\n }\n return;\n }\n\n // Use semantic matching instead of positional\n const matchMap = matchSiblings(fromChildren, toChildren);\n\n // Process matched pairs\n const processedIncoming = new Set<Element>();\n \n for (const [currentChild, incomingChild] of matchMap) {\n if (!incomingChild) {\n // Remove unmatched current node\n from.removeChild(currentChild);\n } else {\n // Recurse into matched pair\n this.morphElement(currentChild, incomingChild, ctx);\n processedIncoming.add(incomingChild);\n }\n }\n\n // Insert new incoming nodes that had no match\n for (let j = 0; j < toChildren.length; j++) {\n const incomingChild = toChildren[j]!;\n if (!processedIncoming.has(incomingChild)) {\n const cloned = incomingChild.cloneNode(true) as Element;\n const refNode = from.children[j] || null;\n from.insertBefore(cloned, refNode);\n }\n }\n }\n}\n\nexport function createMorpher(config?: Partial<MorphConfig>): DOMmorpher {\n return new DOMmorpher(config);\n}\n\nexport function morph(\n from: Element,\n to: Element,\n config?: Partial<MorphConfig>\n): void {\n const morpher = createMorpher(config);\n morpher.morph(from, to);\n}\n"]}