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/CHANGELOG.md +12 -0
- package/dist/index.js +149 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -72
- package/dist/index.mjs.map +1 -1
- package/dist/morpher.d.cts +0 -3
- package/dist/morpher.d.ts +0 -3
- package/dist/morpher.js +149 -72
- package/dist/morpher.js.map +1 -1
- package/dist/morpher.mjs +149 -72
- package/dist/morpher.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
95
|
-
const toChildren = Array.from(to.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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) {
|
package/dist/morpher.js.map
CHANGED
|
@@ -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.
|
|
93
|
-
const toChildren = Array.from(to.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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) {
|
package/dist/morpher.mjs.map
CHANGED
|
@@ -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":"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","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"]}
|