@haklex/rich-diff-core 0.0.94
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/compute-diff.d.ts +19 -0
- package/dist/compute-diff.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +455 -0
- package/package.json +40 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SerializedEditorState, SerializedLexicalNode } from 'lexical';
|
|
2
|
+
export type DiffOpType = 'equal' | 'insert' | 'delete';
|
|
3
|
+
export interface DiffHunk {
|
|
4
|
+
nodes: SerializedLexicalNode[];
|
|
5
|
+
type: DiffOpType;
|
|
6
|
+
}
|
|
7
|
+
export type MarkKind = 'insert' | 'delete';
|
|
8
|
+
export declare function decorateSubtree(node: SerializedLexicalNode, kind: MarkKind): SerializedLexicalNode;
|
|
9
|
+
export declare function diffModifiedNode(oldNode: SerializedLexicalNode, newNode: SerializedLexicalNode): {
|
|
10
|
+
oldNode: SerializedLexicalNode;
|
|
11
|
+
newNode: SerializedLexicalNode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Produces a **single** merged node with both deletions (strikethrough) and
|
|
15
|
+
* insertions (green highlight) interleaved inline – like VS Code's inline diff.
|
|
16
|
+
*/
|
|
17
|
+
export declare function diffMergedNode(oldNode: SerializedLexicalNode, newNode: SerializedLexicalNode): SerializedLexicalNode;
|
|
18
|
+
export declare function computeDiff(oldState: SerializedEditorState, newState: SerializedEditorState): DiffHunk[];
|
|
19
|
+
//# sourceMappingURL=compute-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compute-diff.d.ts","sourceRoot":"","sources":["../src/compute-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAE5E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,IAAI,EAAE,UAAU,CAAC;CAClB;AAUD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AA+E3C,wBAAgB,eAAe,CAC7B,IAAI,EAAE,qBAAqB,EAC3B,IAAI,EAAE,QAAQ,GACb,qBAAqB,CAYvB;AAyXD,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,qBAAqB,GAC7B;IACD,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,qBAAqB,CAAC;CAChC,CAUA;AAkED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,qBAAqB,GAC7B,qBAAqB,CAgBvB;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,qBAAqB,EAC/B,QAAQ,EAAE,qBAAqB,GAC9B,QAAQ,EAAE,CA6CZ"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
const CHAR_DIFF_MAX_MATRIX_CELLS = 5e4;
|
|
2
|
+
const DELETE_MARK_STYLE = "background-color: color-mix(in srgb, var(--rc-alert-caution) 22%, transparent); text-decoration: line-through;";
|
|
3
|
+
const INSERT_MARK_STYLE = "background-color: color-mix(in srgb, var(--rc-alert-tip) 22%, transparent);";
|
|
4
|
+
function getNodeTypeKey(node) {
|
|
5
|
+
const n = node;
|
|
6
|
+
if (n.type === "heading") return `heading:${n.tag}`;
|
|
7
|
+
if (n.type === "list") return `list:${n.listType}`;
|
|
8
|
+
return n.type || "unknown";
|
|
9
|
+
}
|
|
10
|
+
function nodesEqual(a, b) {
|
|
11
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
+
}
|
|
13
|
+
function isTextNode(node) {
|
|
14
|
+
const n = node;
|
|
15
|
+
return n.type === "text" && typeof n.text === "string";
|
|
16
|
+
}
|
|
17
|
+
function getChildren(node) {
|
|
18
|
+
const n = node;
|
|
19
|
+
return Array.isArray(n.children) ? n.children : null;
|
|
20
|
+
}
|
|
21
|
+
function appendStyle(baseStyle, extraStyle) {
|
|
22
|
+
const normalizedBase = typeof baseStyle === "string" ? baseStyle.trim() : "";
|
|
23
|
+
if (!normalizedBase) return extraStyle;
|
|
24
|
+
return `${normalizedBase}${normalizedBase.endsWith(";") ? "" : ";"} ${extraStyle}`;
|
|
25
|
+
}
|
|
26
|
+
function cloneTextNode(node, text, markKind) {
|
|
27
|
+
let { style } = node;
|
|
28
|
+
if (markKind === "delete") {
|
|
29
|
+
style = appendStyle(style, DELETE_MARK_STYLE);
|
|
30
|
+
} else if (markKind === "insert") {
|
|
31
|
+
style = appendStyle(style, INSERT_MARK_STYLE);
|
|
32
|
+
}
|
|
33
|
+
const cloned = {
|
|
34
|
+
...node,
|
|
35
|
+
text,
|
|
36
|
+
style
|
|
37
|
+
};
|
|
38
|
+
return cloned;
|
|
39
|
+
}
|
|
40
|
+
function cloneNodeWithChildren(node, children) {
|
|
41
|
+
return {
|
|
42
|
+
...node,
|
|
43
|
+
children
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function decorateSubtree(node, kind) {
|
|
47
|
+
if (isTextNode(node)) {
|
|
48
|
+
return cloneTextNode(node, node.text, kind);
|
|
49
|
+
}
|
|
50
|
+
const children = getChildren(node);
|
|
51
|
+
if (!children) return node;
|
|
52
|
+
return cloneNodeWithChildren(
|
|
53
|
+
node,
|
|
54
|
+
children.map((child) => decorateSubtree(child, kind))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function reverseText(value) {
|
|
58
|
+
return Array.from(value).reverse().join("");
|
|
59
|
+
}
|
|
60
|
+
function mergeTextOps(ops) {
|
|
61
|
+
const merged = [];
|
|
62
|
+
for (const op of ops) {
|
|
63
|
+
if (!op.text) continue;
|
|
64
|
+
const last = merged.at(-1);
|
|
65
|
+
if (last && last.kind === op.kind) {
|
|
66
|
+
last.text += op.text;
|
|
67
|
+
} else {
|
|
68
|
+
merged.push({ ...op });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
function diffMiddleChars(oldChars, newChars) {
|
|
74
|
+
const m = oldChars.length;
|
|
75
|
+
const n = newChars.length;
|
|
76
|
+
if (m === 0 && n === 0) return [];
|
|
77
|
+
if (m === 0) return [{ kind: "insert", text: newChars.join("") }];
|
|
78
|
+
if (n === 0) return [{ kind: "delete", text: oldChars.join("") }];
|
|
79
|
+
if (m * n > CHAR_DIFF_MAX_MATRIX_CELLS) {
|
|
80
|
+
return [
|
|
81
|
+
{ kind: "delete", text: oldChars.join("") },
|
|
82
|
+
{ kind: "insert", text: newChars.join("") }
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
const dp = Array.from(
|
|
86
|
+
{ length: m + 1 },
|
|
87
|
+
() => Array.from({ length: n + 1 }).fill(0)
|
|
88
|
+
);
|
|
89
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
90
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
91
|
+
if (oldChars[i2 - 1] === newChars[j2 - 1]) {
|
|
92
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
93
|
+
} else {
|
|
94
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const reversedOps = [];
|
|
99
|
+
let i = m;
|
|
100
|
+
let j = n;
|
|
101
|
+
while (i > 0 && j > 0) {
|
|
102
|
+
if (oldChars[i - 1] === newChars[j - 1]) {
|
|
103
|
+
const last = reversedOps.at(-1);
|
|
104
|
+
if (last && last.kind === "equal") {
|
|
105
|
+
last.text += oldChars[i - 1];
|
|
106
|
+
} else {
|
|
107
|
+
reversedOps.push({ kind: "equal", text: oldChars[i - 1] });
|
|
108
|
+
}
|
|
109
|
+
i--;
|
|
110
|
+
j--;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
114
|
+
const last = reversedOps.at(-1);
|
|
115
|
+
if (last && last.kind === "delete") {
|
|
116
|
+
last.text += oldChars[i - 1];
|
|
117
|
+
} else {
|
|
118
|
+
reversedOps.push({ kind: "delete", text: oldChars[i - 1] });
|
|
119
|
+
}
|
|
120
|
+
i--;
|
|
121
|
+
} else {
|
|
122
|
+
const last = reversedOps.at(-1);
|
|
123
|
+
if (last && last.kind === "insert") {
|
|
124
|
+
last.text += newChars[j - 1];
|
|
125
|
+
} else {
|
|
126
|
+
reversedOps.push({ kind: "insert", text: newChars[j - 1] });
|
|
127
|
+
}
|
|
128
|
+
j--;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
while (i > 0) {
|
|
132
|
+
const last = reversedOps.at(-1);
|
|
133
|
+
if (last && last.kind === "delete") {
|
|
134
|
+
last.text += oldChars[i - 1];
|
|
135
|
+
} else {
|
|
136
|
+
reversedOps.push({ kind: "delete", text: oldChars[i - 1] });
|
|
137
|
+
}
|
|
138
|
+
i--;
|
|
139
|
+
}
|
|
140
|
+
while (j > 0) {
|
|
141
|
+
const last = reversedOps.at(-1);
|
|
142
|
+
if (last && last.kind === "insert") {
|
|
143
|
+
last.text += newChars[j - 1];
|
|
144
|
+
} else {
|
|
145
|
+
reversedOps.push({ kind: "insert", text: newChars[j - 1] });
|
|
146
|
+
}
|
|
147
|
+
j--;
|
|
148
|
+
}
|
|
149
|
+
const forwardOps = reversedOps.reverse().map((op) => ({ kind: op.kind, text: reverseText(op.text) }));
|
|
150
|
+
return mergeTextOps(forwardOps);
|
|
151
|
+
}
|
|
152
|
+
function diffTextByChar(oldText, newText) {
|
|
153
|
+
const oldChars = Array.from(oldText);
|
|
154
|
+
const newChars = Array.from(newText);
|
|
155
|
+
let prefix = 0;
|
|
156
|
+
while (prefix < oldChars.length && prefix < newChars.length && oldChars[prefix] === newChars[prefix]) {
|
|
157
|
+
prefix++;
|
|
158
|
+
}
|
|
159
|
+
let oldSuffix = oldChars.length - 1;
|
|
160
|
+
let newSuffix = newChars.length - 1;
|
|
161
|
+
while (oldSuffix >= prefix && newSuffix >= prefix && oldChars[oldSuffix] === newChars[newSuffix]) {
|
|
162
|
+
oldSuffix--;
|
|
163
|
+
newSuffix--;
|
|
164
|
+
}
|
|
165
|
+
const ops = [];
|
|
166
|
+
if (prefix > 0) {
|
|
167
|
+
ops.push({
|
|
168
|
+
kind: "equal",
|
|
169
|
+
text: oldChars.slice(0, prefix).join("")
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
ops.push(
|
|
173
|
+
...diffMiddleChars(
|
|
174
|
+
oldChars.slice(prefix, oldSuffix + 1),
|
|
175
|
+
newChars.slice(prefix, newSuffix + 1)
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
if (oldSuffix < oldChars.length - 1) {
|
|
179
|
+
ops.push({
|
|
180
|
+
kind: "equal",
|
|
181
|
+
text: oldChars.slice(oldSuffix + 1).join("")
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return mergeTextOps(ops);
|
|
185
|
+
}
|
|
186
|
+
function splitTextNodeByCharDiff(oldNode, newNode) {
|
|
187
|
+
const ops = diffTextByChar(oldNode.text, newNode.text);
|
|
188
|
+
const oldNodes = [];
|
|
189
|
+
const newNodes = [];
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const op of ops) {
|
|
192
|
+
if (!op.text) continue;
|
|
193
|
+
if (op.kind === "equal") {
|
|
194
|
+
oldNodes.push(cloneTextNode(oldNode, op.text));
|
|
195
|
+
newNodes.push(cloneTextNode(newNode, op.text));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
changed = true;
|
|
199
|
+
if (op.kind === "delete") {
|
|
200
|
+
oldNodes.push(cloneTextNode(oldNode, op.text, "delete"));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
newNodes.push(cloneTextNode(newNode, op.text, "insert"));
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
oldNodes: oldNodes.length > 0 ? oldNodes : [oldNode],
|
|
207
|
+
newNodes: newNodes.length > 0 ? newNodes : [newNode],
|
|
208
|
+
changed
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function alignNodes(oldNodes, newNodes) {
|
|
212
|
+
const m = oldNodes.length;
|
|
213
|
+
const n = newNodes.length;
|
|
214
|
+
const dp = Array.from(
|
|
215
|
+
{ length: m + 1 },
|
|
216
|
+
() => Array.from({ length: n + 1 }).fill(0)
|
|
217
|
+
);
|
|
218
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
219
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
220
|
+
const skip = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
221
|
+
if (getNodeTypeKey(oldNodes[i2 - 1]) === getNodeTypeKey(newNodes[j2 - 1])) {
|
|
222
|
+
const score = nodesEqual(oldNodes[i2 - 1], newNodes[j2 - 1]) ? 2 : 1;
|
|
223
|
+
dp[i2][j2] = Math.max(skip, dp[i2 - 1][j2 - 1] + score);
|
|
224
|
+
} else {
|
|
225
|
+
dp[i2][j2] = skip;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const ops = [];
|
|
230
|
+
let i = m;
|
|
231
|
+
let j = n;
|
|
232
|
+
while (i > 0 && j > 0) {
|
|
233
|
+
const typeMatch = getNodeTypeKey(oldNodes[i - 1]) === getNodeTypeKey(newNodes[j - 1]);
|
|
234
|
+
if (typeMatch) {
|
|
235
|
+
const exact = nodesEqual(oldNodes[i - 1], newNodes[j - 1]);
|
|
236
|
+
const score = exact ? 2 : 1;
|
|
237
|
+
if (dp[i][j] === dp[i - 1][j - 1] + score) {
|
|
238
|
+
ops.push(
|
|
239
|
+
exact ? { kind: "equal", node: oldNodes[i - 1] } : {
|
|
240
|
+
kind: "modify",
|
|
241
|
+
oldNode: oldNodes[i - 1],
|
|
242
|
+
newNode: newNodes[j - 1]
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
i--;
|
|
246
|
+
j--;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
251
|
+
ops.push({ kind: "delete", node: oldNodes[i - 1] });
|
|
252
|
+
i--;
|
|
253
|
+
} else {
|
|
254
|
+
ops.push({ kind: "insert", node: newNodes[j - 1] });
|
|
255
|
+
j--;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
while (i > 0) {
|
|
259
|
+
ops.push({ kind: "delete", node: oldNodes[--i] });
|
|
260
|
+
}
|
|
261
|
+
while (j > 0) {
|
|
262
|
+
ops.push({ kind: "insert", node: newNodes[--j] });
|
|
263
|
+
}
|
|
264
|
+
ops.reverse();
|
|
265
|
+
return ops;
|
|
266
|
+
}
|
|
267
|
+
function diffChildrenInline(oldChildren, newChildren) {
|
|
268
|
+
const ops = alignNodes(oldChildren, newChildren);
|
|
269
|
+
const nextOldChildren = [];
|
|
270
|
+
const nextNewChildren = [];
|
|
271
|
+
let changed = false;
|
|
272
|
+
for (const op of ops) {
|
|
273
|
+
switch (op.kind) {
|
|
274
|
+
case "equal": {
|
|
275
|
+
nextOldChildren.push(op.node);
|
|
276
|
+
nextNewChildren.push(op.node);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "delete": {
|
|
280
|
+
changed = true;
|
|
281
|
+
nextOldChildren.push(decorateSubtree(op.node, "delete"));
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "insert": {
|
|
285
|
+
changed = true;
|
|
286
|
+
nextNewChildren.push(decorateSubtree(op.node, "insert"));
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case "modify": {
|
|
290
|
+
changed = true;
|
|
291
|
+
if (isTextNode(op.oldNode) && isTextNode(op.newNode)) {
|
|
292
|
+
const textDiff = splitTextNodeByCharDiff(op.oldNode, op.newNode);
|
|
293
|
+
nextOldChildren.push(...textDiff.oldNodes);
|
|
294
|
+
nextNewChildren.push(...textDiff.newNodes);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
const nested = diffNodeInline(op.oldNode, op.newNode);
|
|
298
|
+
if (nested) {
|
|
299
|
+
nextOldChildren.push(nested.oldNode);
|
|
300
|
+
nextNewChildren.push(nested.newNode);
|
|
301
|
+
} else {
|
|
302
|
+
nextOldChildren.push(decorateSubtree(op.oldNode, "delete"));
|
|
303
|
+
nextNewChildren.push(decorateSubtree(op.newNode, "insert"));
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
oldChildren: nextOldChildren,
|
|
311
|
+
newChildren: nextNewChildren,
|
|
312
|
+
changed
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function diffNodeInline(oldNode, newNode) {
|
|
316
|
+
if (getNodeTypeKey(oldNode) !== getNodeTypeKey(newNode)) return null;
|
|
317
|
+
if (nodesEqual(oldNode, newNode)) {
|
|
318
|
+
return { oldNode, newNode, changed: false };
|
|
319
|
+
}
|
|
320
|
+
const oldChildren = getChildren(oldNode);
|
|
321
|
+
const newChildren = getChildren(newNode);
|
|
322
|
+
if (!oldChildren || !newChildren) return null;
|
|
323
|
+
const childDiff = diffChildrenInline(oldChildren, newChildren);
|
|
324
|
+
if (!childDiff.changed) {
|
|
325
|
+
return { oldNode, newNode, changed: false };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
oldNode: cloneNodeWithChildren(oldNode, childDiff.oldChildren),
|
|
329
|
+
newNode: cloneNodeWithChildren(newNode, childDiff.newChildren),
|
|
330
|
+
changed: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function diffModifiedNode(oldNode, newNode) {
|
|
334
|
+
const nested = diffNodeInline(oldNode, newNode);
|
|
335
|
+
if (nested) {
|
|
336
|
+
return { oldNode: nested.oldNode, newNode: nested.newNode };
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
oldNode: decorateSubtree(oldNode, "delete"),
|
|
340
|
+
newNode: decorateSubtree(newNode, "insert")
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function mergeTextDiffNodes(oldNode, newNode) {
|
|
344
|
+
const ops = diffTextByChar(oldNode.text, newNode.text);
|
|
345
|
+
const merged = [];
|
|
346
|
+
for (const op of ops) {
|
|
347
|
+
if (!op.text) continue;
|
|
348
|
+
if (op.kind === "equal") {
|
|
349
|
+
merged.push(cloneTextNode(newNode, op.text));
|
|
350
|
+
} else if (op.kind === "delete") {
|
|
351
|
+
merged.push(cloneTextNode(oldNode, op.text, "delete"));
|
|
352
|
+
} else {
|
|
353
|
+
merged.push(cloneTextNode(newNode, op.text, "insert"));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return merged;
|
|
357
|
+
}
|
|
358
|
+
function diffChildrenMerged(oldChildren, newChildren) {
|
|
359
|
+
const ops = alignNodes(oldChildren, newChildren);
|
|
360
|
+
const merged = [];
|
|
361
|
+
for (const op of ops) {
|
|
362
|
+
switch (op.kind) {
|
|
363
|
+
case "equal": {
|
|
364
|
+
merged.push(op.node);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "delete": {
|
|
368
|
+
merged.push(decorateSubtree(op.node, "delete"));
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case "insert": {
|
|
372
|
+
merged.push(decorateSubtree(op.node, "insert"));
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "modify": {
|
|
376
|
+
if (isTextNode(op.oldNode) && isTextNode(op.newNode)) {
|
|
377
|
+
merged.push(...mergeTextDiffNodes(op.oldNode, op.newNode));
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
const nestedOld = getChildren(op.oldNode);
|
|
381
|
+
const nestedNew = getChildren(op.newNode);
|
|
382
|
+
if (nestedOld && nestedNew) {
|
|
383
|
+
merged.push(cloneNodeWithChildren(op.newNode, diffChildrenMerged(nestedOld, nestedNew)));
|
|
384
|
+
} else {
|
|
385
|
+
merged.push(decorateSubtree(op.oldNode, "delete"));
|
|
386
|
+
merged.push(decorateSubtree(op.newNode, "insert"));
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return merged;
|
|
393
|
+
}
|
|
394
|
+
function diffMergedNode(oldNode, newNode) {
|
|
395
|
+
if (getNodeTypeKey(oldNode) !== getNodeTypeKey(newNode)) {
|
|
396
|
+
return decorateSubtree(newNode, "insert");
|
|
397
|
+
}
|
|
398
|
+
if (nodesEqual(oldNode, newNode)) {
|
|
399
|
+
return newNode;
|
|
400
|
+
}
|
|
401
|
+
const oldChildren = getChildren(oldNode);
|
|
402
|
+
const newChildren = getChildren(newNode);
|
|
403
|
+
if (!oldChildren || !newChildren) {
|
|
404
|
+
return decorateSubtree(newNode, "insert");
|
|
405
|
+
}
|
|
406
|
+
return cloneNodeWithChildren(newNode, diffChildrenMerged(oldChildren, newChildren));
|
|
407
|
+
}
|
|
408
|
+
function computeDiff(oldState, newState) {
|
|
409
|
+
const ops = alignNodes(oldState.root.children, newState.root.children);
|
|
410
|
+
const hunks = [];
|
|
411
|
+
for (const op of ops) {
|
|
412
|
+
switch (op.kind) {
|
|
413
|
+
case "equal": {
|
|
414
|
+
const last = hunks.at(-1);
|
|
415
|
+
if (last && last.type === "equal") {
|
|
416
|
+
last.nodes.push(op.node);
|
|
417
|
+
} else {
|
|
418
|
+
hunks.push({ type: "equal", nodes: [op.node] });
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "modify": {
|
|
423
|
+
{
|
|
424
|
+
const inline = diffModifiedNode(op.oldNode, op.newNode);
|
|
425
|
+
hunks.push(
|
|
426
|
+
{ type: "delete", nodes: [inline.oldNode] },
|
|
427
|
+
{ type: "insert", nodes: [inline.newNode] }
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "delete": {
|
|
433
|
+
hunks.push({
|
|
434
|
+
type: "delete",
|
|
435
|
+
nodes: [decorateSubtree(op.node, "delete")]
|
|
436
|
+
});
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "insert": {
|
|
440
|
+
hunks.push({
|
|
441
|
+
type: "insert",
|
|
442
|
+
nodes: [decorateSubtree(op.node, "insert")]
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return hunks;
|
|
449
|
+
}
|
|
450
|
+
export {
|
|
451
|
+
computeDiff,
|
|
452
|
+
decorateSubtree,
|
|
453
|
+
diffMergedNode,
|
|
454
|
+
diffModifiedNode
|
|
455
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haklex/rich-diff-core",
|
|
3
|
+
"version": "0.0.94",
|
|
4
|
+
"description": "Lexical diff engine — character-level and structural diff with node decoration",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Innei/haklex.git",
|
|
8
|
+
"directory": "packages/rich-diff-core"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.mjs",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"lexical": "^0.42.0",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vite": "^7.3.1",
|
|
27
|
+
"vite-plugin-dts": "^4.5.4"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"lexical": "^0.42.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "vite build",
|
|
37
|
+
"dev:build": "vite build --watch"
|
|
38
|
+
},
|
|
39
|
+
"types": "./dist/index.d.ts"
|
|
40
|
+
}
|