@sanity/diff-patch 6.0.0 → 7.0.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/README.md +8 -1
- package/dist/index.d.ts +99 -132
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +365 -224
- package/dist/index.js.map +1 -1
- package/package.json +56 -77
- package/src/diffPatch.ts +7 -15
- package/src/patches.ts +2 -2
- package/dist/index.cjs +0 -271
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -195
package/dist/index.js
CHANGED
|
@@ -1,271 +1,412 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makePatches, stringifyPatches } from "@sanity/diff-match-patch";
|
|
2
2
|
const IS_DOTTABLE_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
+
/**
|
|
4
|
+
* Converts an array path to a string path
|
|
5
|
+
*
|
|
6
|
+
* @param path - The array path to convert
|
|
7
|
+
* @returns A stringified path
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
3
10
|
function pathToString(path) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return `${target}['${segment}']`;
|
|
13
|
-
if (typeof segment == "string")
|
|
14
|
-
return `${target}${i === 0 ? "" : "."}${segment}`;
|
|
15
|
-
throw new Error(`Unsupported path segment "${segment}"`);
|
|
16
|
-
}, "");
|
|
11
|
+
return path.reduce((target, segment, i) => {
|
|
12
|
+
if (Array.isArray(segment)) return `${target}[${segment.join(":")}]`;
|
|
13
|
+
if (isKeyedObject(segment)) return `${target}[_key=="${segment._key}"]`;
|
|
14
|
+
if (typeof segment == "number") return `${target}[${segment}]`;
|
|
15
|
+
if (typeof segment == "string" && !IS_DOTTABLE_RE.test(segment)) return `${target}['${segment}']`;
|
|
16
|
+
if (typeof segment == "string") return `${target}${i === 0 ? "" : "."}${segment}`;
|
|
17
|
+
throw Error(`Unsupported path segment "${segment}"`);
|
|
18
|
+
}, "");
|
|
17
19
|
}
|
|
18
20
|
function isKeyedObject(obj) {
|
|
19
|
-
|
|
21
|
+
return typeof obj == "object" && !!obj && "_key" in obj && typeof obj._key == "string";
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
23
|
+
function _typeof(o) {
|
|
24
|
+
"@babel/helpers - typeof";
|
|
25
|
+
return _typeof = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(o) {
|
|
26
|
+
return typeof o;
|
|
27
|
+
} : function(o) {
|
|
28
|
+
return o && typeof Symbol == "function" && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
29
|
+
}, _typeof(o);
|
|
29
30
|
}
|
|
31
|
+
function toPrimitive(t, r) {
|
|
32
|
+
if (_typeof(t) != "object" || !t) return t;
|
|
33
|
+
var e = t[Symbol.toPrimitive];
|
|
34
|
+
if (e !== void 0) {
|
|
35
|
+
var i = e.call(t, r || "default");
|
|
36
|
+
if (_typeof(i) != "object") return i;
|
|
37
|
+
throw TypeError("@@toPrimitive must return a primitive value.");
|
|
38
|
+
}
|
|
39
|
+
return (r === "string" ? String : Number)(t);
|
|
40
|
+
}
|
|
41
|
+
function toPropertyKey(t) {
|
|
42
|
+
var i = toPrimitive(t, "string");
|
|
43
|
+
return _typeof(i) == "symbol" ? i : i + "";
|
|
44
|
+
}
|
|
45
|
+
function _defineProperty(e, r, t) {
|
|
46
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
47
|
+
value: t,
|
|
48
|
+
enumerable: !0,
|
|
49
|
+
configurable: !0,
|
|
50
|
+
writable: !0
|
|
51
|
+
}) : e[r] = t, e;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Represents an error that occurred during a diff process.
|
|
55
|
+
* Contains `path`, `value` and `serializedPath` properties,
|
|
56
|
+
* which is helpful for debugging and showing friendly messages.
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
var DiffError = class extends Error {
|
|
61
|
+
constructor(message, path, value) {
|
|
62
|
+
let serializedPath = pathToString(path);
|
|
63
|
+
super(`${message} (at '${serializedPath}')`), _defineProperty(this, "path", void 0), _defineProperty(this, "value", void 0), _defineProperty(this, "serializedPath", void 0), this.path = path, this.serializedPath = serializedPath, this.value = value;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
30
66
|
const idPattern = /^[a-z0-9][a-z0-9_.-]+$/i, propPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/, propStartPattern = /^[a-z_]/i;
|
|
67
|
+
/**
|
|
68
|
+
* Validate a property for Sanity compatibility
|
|
69
|
+
*
|
|
70
|
+
* @param property - The property to valide
|
|
71
|
+
* @param value - The value of the property
|
|
72
|
+
* @param path - The path of the property, for error reporting
|
|
73
|
+
* @returns The property name, if valid
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
31
76
|
function validateProperty(property, value, path) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
if (property === "_key" || property === "_ref" || property === "_type") {
|
|
41
|
-
if (typeof value != "string")
|
|
42
|
-
throw new DiffError("Keys must be strings", path.concat(property), value);
|
|
43
|
-
if (!idPattern.test(value))
|
|
44
|
-
throw new DiffError("Invalid key - use less exotic characters", path.concat(property), value);
|
|
45
|
-
}
|
|
46
|
-
return property;
|
|
77
|
+
if (!propStartPattern.test(property)) throw new DiffError("Keys must start with a letter (a-z)", path.concat(property), value);
|
|
78
|
+
if (!propPattern.test(property)) throw new DiffError("Keys can only contain letters, numbers and underscores", path.concat(property), value);
|
|
79
|
+
if (property === "_key" || property === "_ref" || property === "_type") {
|
|
80
|
+
if (typeof value != "string") throw new DiffError("Keys must be strings", path.concat(property), value);
|
|
81
|
+
if (!idPattern.test(value)) throw new DiffError("Invalid key - use less exotic characters", path.concat(property), value);
|
|
82
|
+
}
|
|
83
|
+
return property;
|
|
47
84
|
}
|
|
48
85
|
function difference(source, target) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
target.has(item) || result.add(item);
|
|
54
|
-
return result;
|
|
86
|
+
if ("difference" in Set.prototype) return source.difference(target);
|
|
87
|
+
let result = /* @__PURE__ */ new Set();
|
|
88
|
+
for (let item of source) target.has(item) || result.add(item);
|
|
89
|
+
return result;
|
|
55
90
|
}
|
|
56
91
|
function intersection(source, target) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
target.has(item) && result.add(item);
|
|
62
|
-
return result;
|
|
92
|
+
if ("intersection" in Set.prototype) return source.intersection(target);
|
|
93
|
+
let result = /* @__PURE__ */ new Set();
|
|
94
|
+
for (let item of source) target.has(item) && result.add(item);
|
|
95
|
+
return result;
|
|
63
96
|
}
|
|
64
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Document keys that are ignored during diff operations.
|
|
99
|
+
* These are system-managed fields that should not be included in patches on
|
|
100
|
+
* top-level documents and should not be diffed with diff-match-patch.
|
|
101
|
+
*/
|
|
102
|
+
const SYSTEM_KEYS = [
|
|
103
|
+
"_id",
|
|
104
|
+
"_type",
|
|
105
|
+
"_createdAt",
|
|
106
|
+
"_updatedAt",
|
|
107
|
+
"_rev"
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* Generates an array of mutations for Sanity, based on the differences between
|
|
111
|
+
* the two passed documents/trees.
|
|
112
|
+
*
|
|
113
|
+
* @param source - The first document/tree to compare
|
|
114
|
+
* @param target - The second document/tree to compare
|
|
115
|
+
* @param opts - Options for the diff generation
|
|
116
|
+
* @returns Array of mutations
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
65
119
|
function diffPatch(source, target, options = {}) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (basePath.length === 0 && source._type !== target._type)
|
|
76
|
-
throw new Error(`_type is immutable and cannot be changed (${source._type} => ${target._type})`);
|
|
77
|
-
const operations = diffItem(source, target, basePath, []);
|
|
78
|
-
return serializePatches(operations).map((patchOperations, i) => ({
|
|
79
|
-
patch: {
|
|
80
|
-
id,
|
|
81
|
-
// only add `ifRevisionID` to the first patch
|
|
82
|
-
...i === 0 && ifRevisionID && { ifRevisionID },
|
|
83
|
-
...patchOperations
|
|
84
|
-
}
|
|
85
|
-
}));
|
|
120
|
+
let id = options.id || source._id === target._id && source._id, revisionLocked = options.ifRevisionID, ifRevisionID = typeof revisionLocked == "boolean" ? source._rev : revisionLocked, basePath = options.basePath || [];
|
|
121
|
+
if (!id) throw Error("_id on source and target not present or differs, specify document id the mutations should be applied to");
|
|
122
|
+
if (revisionLocked === !0 && !ifRevisionID) throw Error("`ifRevisionID` is set to `true`, but no `_rev` was passed in item A. Either explicitly set `ifRevisionID` to a revision, or pass `_rev` as part of item A.");
|
|
123
|
+
if (basePath.length === 0 && source._type !== target._type) throw Error(`_type is immutable and cannot be changed (${source._type} => ${target._type})`);
|
|
124
|
+
return serializePatches(diffItem(source, target, basePath, [])).map((patchOperations, i) => ({ patch: {
|
|
125
|
+
id,
|
|
126
|
+
...i === 0 && ifRevisionID && { ifRevisionID },
|
|
127
|
+
...patchOperations
|
|
128
|
+
} }));
|
|
86
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Generates an array of patch operation objects for Sanity, based on the
|
|
132
|
+
* differences between the two passed values
|
|
133
|
+
*
|
|
134
|
+
* @param source - The source value to start off with
|
|
135
|
+
* @param target - The target value that the patch operations will aim to create
|
|
136
|
+
* @param basePath - An optional path that will be prefixed to all subsequent patch operations
|
|
137
|
+
* @returns Array of mutations
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
87
140
|
function diffValue(source, target, basePath = []) {
|
|
88
|
-
|
|
141
|
+
return serializePatches(diffItem(source, target, basePath));
|
|
89
142
|
}
|
|
90
143
|
function diffItem(source, target, path = [], patches = []) {
|
|
91
|
-
|
|
144
|
+
return source === target ? patches : typeof source == "string" && typeof target == "string" ? (diffString(source, target, path, patches), patches) : Array.isArray(source) && Array.isArray(target) ? (diffArray(source, target, path, patches), patches) : isRecord(source) && isRecord(target) ? (diffObject(source, target, path, patches), patches) : target === void 0 ? (patches.push({
|
|
145
|
+
op: "unset",
|
|
146
|
+
path
|
|
147
|
+
}), patches) : (patches.push({
|
|
148
|
+
op: "set",
|
|
149
|
+
path,
|
|
150
|
+
value: target
|
|
151
|
+
}), patches);
|
|
92
152
|
}
|
|
93
153
|
function diffObject(source, target, path, patches) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
154
|
+
let atRoot = path.length === 0, aKeys = Object.keys(source).filter(atRoot ? isNotIgnoredKey : yes).map((key) => validateProperty(key, source[key], path)), aKeysLength = aKeys.length, bKeys = Object.keys(target).filter(atRoot ? isNotIgnoredKey : yes).map((key) => validateProperty(key, target[key], path)), bKeysLength = bKeys.length;
|
|
155
|
+
for (let i = 0; i < aKeysLength; i++) {
|
|
156
|
+
let key = aKeys[i];
|
|
157
|
+
key in target || patches.push({
|
|
158
|
+
op: "unset",
|
|
159
|
+
path: path.concat(key)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < bKeysLength; i++) {
|
|
163
|
+
let key = bKeys[i];
|
|
164
|
+
diffItem(source[key], target[key], path.concat([key]), patches);
|
|
165
|
+
}
|
|
166
|
+
return patches;
|
|
104
167
|
}
|
|
105
168
|
function diffArray(source, target, path, patches) {
|
|
106
|
-
|
|
169
|
+
return isUniquelyKeyed(source) && isUniquelyKeyed(target) ? diffArrayByKey(source, target, path, patches) : diffArrayByIndex(source, target, path, patches);
|
|
107
170
|
}
|
|
108
171
|
function diffArrayByIndex(source, target, path, patches) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const overlapping = Math.min(source.length, target.length), segmentA = source.slice(0, overlapping), segmentB = target.slice(0, overlapping);
|
|
129
|
-
for (let i = 0; i < segmentA.length; i++)
|
|
130
|
-
diffItem(segmentA[i], nullifyUndefined(segmentB[i]), path.concat(i), patches);
|
|
131
|
-
return patches;
|
|
172
|
+
if (target.length > source.length && patches.push({
|
|
173
|
+
op: "insert",
|
|
174
|
+
position: "after",
|
|
175
|
+
path: path.concat([-1]),
|
|
176
|
+
items: target.slice(source.length).map(nullifyUndefined)
|
|
177
|
+
}), target.length < source.length) {
|
|
178
|
+
let isSingle = source.length - target.length === 1, unsetItems = source.slice(target.length);
|
|
179
|
+
isUniquelyKeyed(unsetItems) ? patches.push(...unsetItems.map((item) => ({
|
|
180
|
+
op: "unset",
|
|
181
|
+
path: path.concat({ _key: item._key })
|
|
182
|
+
}))) : patches.push({
|
|
183
|
+
op: "unset",
|
|
184
|
+
path: path.concat([isSingle ? target.length : [target.length, ""]])
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (let i = 0; i < target.length; i++) if (Array.isArray(target[i])) throw new DiffError("Multi-dimensional arrays not supported", path.concat(i), target[i]);
|
|
188
|
+
let overlapping = Math.min(source.length, target.length), segmentA = source.slice(0, overlapping), segmentB = target.slice(0, overlapping);
|
|
189
|
+
for (let i = 0; i < segmentA.length; i++) diffItem(segmentA[i], nullifyUndefined(segmentB[i]), path.concat(i), patches);
|
|
190
|
+
return patches;
|
|
132
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Diffs two arrays of keyed objects by their `_key` properties.
|
|
194
|
+
*
|
|
195
|
+
* This approach is preferred over index-based diffing for collaborative editing scenarios
|
|
196
|
+
* because it generates patches that are more resilient to concurrent modifications.
|
|
197
|
+
* When multiple users edit the same array simultaneously, key-based patches have better
|
|
198
|
+
* conflict resolution characteristics than index-based patches.
|
|
199
|
+
*
|
|
200
|
+
* The function handles three main operations:
|
|
201
|
+
* 1. **Reordering**: When existing items change positions within the array
|
|
202
|
+
* 2. **Content changes**: When the content of existing items is modified
|
|
203
|
+
* 3. **Insertions/Deletions**: When items are added or removed from the array
|
|
204
|
+
*
|
|
205
|
+
* @param source - The original array with keyed objects
|
|
206
|
+
* @param target - The target array with keyed objects
|
|
207
|
+
* @param path - The path to this array within the document
|
|
208
|
+
* @param patches - Array to accumulate generated patches
|
|
209
|
+
* @returns The patches array with new patches appended
|
|
210
|
+
*/
|
|
133
211
|
function diffArrayByKey(source, target, path, patches) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
let sourceItemsByKey = new Map(source.map((item) => [item._key, item])), targetItemsByKey = new Map(target.map((item) => [item._key, item])), sourceKeys = new Set(sourceItemsByKey.keys()), targetKeys = new Set(targetItemsByKey.keys()), keysRemovedFromSource = difference(sourceKeys, targetKeys), keysAddedToTarget = difference(targetKeys, sourceKeys), keysInBothArrays = intersection(sourceKeys, targetKeys), sourceKeysStillPresent = Array.from(difference(sourceKeys, keysRemovedFromSource)), targetKeysAlreadyPresent = Array.from(difference(targetKeys, keysAddedToTarget)), keyReorderOperations = [];
|
|
213
|
+
for (let i = 0; i < keysInBothArrays.size; i++) {
|
|
214
|
+
let keyAtPositionInSource = sourceKeysStillPresent[i], keyAtPositionInTarget = targetKeysAlreadyPresent[i];
|
|
215
|
+
keyAtPositionInSource !== keyAtPositionInTarget && keyReorderOperations.push({
|
|
216
|
+
sourceKey: keyAtPositionInSource,
|
|
217
|
+
targetKey: keyAtPositionInTarget
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
keyReorderOperations.length && patches.push({
|
|
221
|
+
op: "reorder",
|
|
222
|
+
path,
|
|
223
|
+
snapshot: source,
|
|
224
|
+
reorders: keyReorderOperations
|
|
225
|
+
});
|
|
226
|
+
for (let key of keysInBothArrays) diffItem(sourceItemsByKey.get(key), targetItemsByKey.get(key), [...path, { _key: key }], patches);
|
|
227
|
+
for (let keyToRemove of keysRemovedFromSource) patches.push({
|
|
228
|
+
op: "unset",
|
|
229
|
+
path: [...path, { _key: keyToRemove }]
|
|
230
|
+
});
|
|
231
|
+
if (keysAddedToTarget.size) {
|
|
232
|
+
let insertionAnchorKey, itemsPendingInsertion = [], flushPendingInsertions = () => {
|
|
233
|
+
itemsPendingInsertion.length && patches.push({
|
|
234
|
+
op: "insert",
|
|
235
|
+
...insertionAnchorKey ? {
|
|
236
|
+
position: "after",
|
|
237
|
+
path: [...path, { _key: insertionAnchorKey }]
|
|
238
|
+
} : {
|
|
239
|
+
position: "before",
|
|
240
|
+
path: [...path, 0]
|
|
241
|
+
},
|
|
242
|
+
items: itemsPendingInsertion
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
for (let key of targetKeys) keysAddedToTarget.has(key) ? itemsPendingInsertion.push(targetItemsByKey.get(key)) : keysInBothArrays.has(key) && (flushPendingInsertions(), insertionAnchorKey = key, itemsPendingInsertion = []);
|
|
246
|
+
flushPendingInsertions();
|
|
247
|
+
}
|
|
248
|
+
return patches;
|
|
167
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Determines whether to use diff-match-patch or fallback to a `set` operation
|
|
252
|
+
* when creating a patch to transform a `source` string to `target` string.
|
|
253
|
+
*
|
|
254
|
+
* `diffMatchPatch` patches are typically preferred to `set` operations because
|
|
255
|
+
* they handle conflicts better (when multiple editors work simultaneously) by
|
|
256
|
+
* preserving the user's intended and allowing for 3-way merges.
|
|
257
|
+
*
|
|
258
|
+
* **Heuristic rationale:**
|
|
259
|
+
*
|
|
260
|
+
* Perf analysis revealed that string length has minimal impact on small,
|
|
261
|
+
* keystroke-level changes, but large text replacements (high change ratio) can
|
|
262
|
+
* trigger worst-case algorithm behavior. The 40% change ratio threshold is a
|
|
263
|
+
* simple heuristic that catches problematic replacement scenarios while
|
|
264
|
+
* allowing the algorithm to excel at insertions and deletions.
|
|
265
|
+
*
|
|
266
|
+
* **Performance characteristics (tested on M2 MacBook Pro):**
|
|
267
|
+
*
|
|
268
|
+
* *Keystroke-level editing (most common use case):*
|
|
269
|
+
* - Small strings (1KB-10KB): 0ms for 1-5 keystrokes, consistently sub-millisecond
|
|
270
|
+
* - Medium strings (50KB-200KB): 0ms for 1-5 keystrokes, consistently sub-millisecond
|
|
271
|
+
* - 10 simultaneous keystrokes: ~12ms on 100KB strings
|
|
272
|
+
*
|
|
273
|
+
* *Copy-paste operations (less frequent):*
|
|
274
|
+
* - Small copy-paste operations (<50KB): 0-10ms regardless of string length
|
|
275
|
+
* - Large insertions/deletions (50KB+): 0-50ms (excellent performance)
|
|
276
|
+
* - Large text replacements (50KB+): 70ms-2s+ (can be slow due to algorithm complexity)
|
|
277
|
+
*
|
|
278
|
+
* **Algorithm details:**
|
|
279
|
+
* Uses Myers' diff algorithm with O(ND) time complexity where N=text length and D=edit distance.
|
|
280
|
+
* Includes optimizations: common prefix/suffix removal, line-mode processing, and timeout protection.
|
|
281
|
+
*
|
|
282
|
+
*
|
|
283
|
+
* **Test methodology:**
|
|
284
|
+
* - Generated realistic word-based text patterns
|
|
285
|
+
* - Simulated actual editing behaviors (keystrokes vs copy-paste)
|
|
286
|
+
* - Measured performance across string sizes from 1KB to 10MB
|
|
287
|
+
* - Validated against edge cases including repetitive text and scattered changes
|
|
288
|
+
*
|
|
289
|
+
* @param source - The previous version of the text
|
|
290
|
+
* @param target - The new version of the text
|
|
291
|
+
* @returns true if diff-match-patch should be used, false if fallback to set operation
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* // Keystroke editing - always fast
|
|
296
|
+
* shouldUseDiffMatchPatch(largeDoc, largeDocWithTypo) // true, ~0ms
|
|
297
|
+
*
|
|
298
|
+
* // Small paste - always fast
|
|
299
|
+
* shouldUseDiffMatchPatch(doc, docWithSmallInsertion) // true, ~0ms
|
|
300
|
+
*
|
|
301
|
+
* // Large replacement - potentially slow
|
|
302
|
+
* shouldUseDiffMatchPatch(article, completelyDifferentArticle) // false, use set
|
|
303
|
+
* ```
|
|
304
|
+
*
|
|
305
|
+
* Compatible with @sanity/diff-match-patch@3.2.0
|
|
306
|
+
*/
|
|
168
307
|
function shouldUseDiffMatchPatch(source, target) {
|
|
169
|
-
|
|
170
|
-
|
|
308
|
+
let maxLength = Math.max(source.length, target.length);
|
|
309
|
+
return maxLength > 1e6 ? !1 : maxLength < 1e4 || !(Math.abs(target.length - source.length) / maxLength > .4);
|
|
171
310
|
}
|
|
172
311
|
function getDiffMatchPatch(source, target, path) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
312
|
+
let last = path.at(-1);
|
|
313
|
+
if (!(typeof last == "string" && last.startsWith("_")) && shouldUseDiffMatchPatch(source, target)) try {
|
|
314
|
+
return {
|
|
315
|
+
op: "diffMatchPatch",
|
|
316
|
+
path,
|
|
317
|
+
value: stringifyPatches(makePatches(source, target))
|
|
318
|
+
};
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
181
322
|
}
|
|
182
323
|
function diffString(source, target, path, patches) {
|
|
183
|
-
|
|
184
|
-
|
|
324
|
+
let dmp = getDiffMatchPatch(source, target, path);
|
|
325
|
+
return patches.push(dmp ?? {
|
|
326
|
+
op: "set",
|
|
327
|
+
path,
|
|
328
|
+
value: target
|
|
329
|
+
}), patches;
|
|
185
330
|
}
|
|
186
331
|
function isNotIgnoredKey(key) {
|
|
187
|
-
|
|
332
|
+
return SYSTEM_KEYS.indexOf(key) === -1;
|
|
188
333
|
}
|
|
189
334
|
function serializePatches(patches, curr) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
default:
|
|
236
|
-
return [];
|
|
237
|
-
}
|
|
335
|
+
let [patch, ...rest] = patches;
|
|
336
|
+
if (!patch) return curr ? [curr] : [];
|
|
337
|
+
switch (patch.op) {
|
|
338
|
+
case "set":
|
|
339
|
+
case "diffMatchPatch": {
|
|
340
|
+
let emptyOp = { [patch.op]: {} };
|
|
341
|
+
return curr ? patch.op in curr ? (Object.assign(curr[patch.op], { [pathToString(patch.path)]: patch.value }), serializePatches(rest, curr)) : [curr, ...serializePatches(patches, emptyOp)] : serializePatches(patches, emptyOp);
|
|
342
|
+
}
|
|
343
|
+
case "unset": {
|
|
344
|
+
let emptyOp = { unset: [] };
|
|
345
|
+
return curr ? "unset" in curr ? (curr.unset.push(pathToString(patch.path)), serializePatches(rest, curr)) : [curr, ...serializePatches(patches, emptyOp)] : serializePatches(patches, emptyOp);
|
|
346
|
+
}
|
|
347
|
+
case "insert": return curr ? [curr, ...serializePatches(patches)] : [{ insert: {
|
|
348
|
+
[patch.position]: pathToString(patch.path),
|
|
349
|
+
items: patch.items
|
|
350
|
+
} }, ...serializePatches(rest)];
|
|
351
|
+
case "reorder": {
|
|
352
|
+
if (curr) return [curr, ...serializePatches(patches)];
|
|
353
|
+
let tempKeyOperations = {};
|
|
354
|
+
tempKeyOperations.set = {};
|
|
355
|
+
for (let { sourceKey, targetKey } of patch.reorders) {
|
|
356
|
+
let temporaryKey = `__temp_reorder_${sourceKey}__`, finalContentForThisPosition = patch.snapshot[getIndexForKey(patch.snapshot, targetKey)];
|
|
357
|
+
Object.assign(tempKeyOperations.set, { [pathToString([...patch.path, { _key: sourceKey }])]: {
|
|
358
|
+
...finalContentForThisPosition,
|
|
359
|
+
_key: temporaryKey
|
|
360
|
+
} });
|
|
361
|
+
}
|
|
362
|
+
let finalKeyOperations = {};
|
|
363
|
+
finalKeyOperations.set = {};
|
|
364
|
+
for (let { sourceKey, targetKey } of patch.reorders) {
|
|
365
|
+
let temporaryKey = `__temp_reorder_${sourceKey}__`;
|
|
366
|
+
Object.assign(finalKeyOperations.set, { [pathToString([
|
|
367
|
+
...patch.path,
|
|
368
|
+
{ _key: temporaryKey },
|
|
369
|
+
"_key"
|
|
370
|
+
])]: targetKey });
|
|
371
|
+
}
|
|
372
|
+
return [
|
|
373
|
+
tempKeyOperations,
|
|
374
|
+
finalKeyOperations,
|
|
375
|
+
...serializePatches(rest)
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
default: return [];
|
|
379
|
+
}
|
|
238
380
|
}
|
|
239
381
|
function isUniquelyKeyed(arr) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
382
|
+
let seenKeys = /* @__PURE__ */ new Set();
|
|
383
|
+
for (let item of arr) {
|
|
384
|
+
if (!isKeyedObject(item) || seenKeys.has(item._key)) return !1;
|
|
385
|
+
seenKeys.add(item._key);
|
|
386
|
+
}
|
|
387
|
+
return !0;
|
|
246
388
|
}
|
|
247
389
|
const keyToIndexCache = /* @__PURE__ */ new WeakMap();
|
|
248
390
|
function getIndexForKey(keyedArray, targetKey) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{}
|
|
254
|
-
);
|
|
255
|
-
return keyToIndexCache.set(keyedArray, keyToIndexMapping), keyToIndexMapping[targetKey];
|
|
391
|
+
let cachedMapping = keyToIndexCache.get(keyedArray);
|
|
392
|
+
if (cachedMapping) return cachedMapping[targetKey];
|
|
393
|
+
let keyToIndexMapping = keyedArray.reduce((mapping, { _key }, arrayIndex) => (mapping[_key] = arrayIndex, mapping), {});
|
|
394
|
+
return keyToIndexCache.set(keyedArray, keyToIndexMapping), keyToIndexMapping[targetKey];
|
|
256
395
|
}
|
|
257
396
|
function isRecord(value) {
|
|
258
|
-
|
|
397
|
+
return typeof value == "object" && !!value && !Array.isArray(value);
|
|
259
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Simplify returns `null` if the value given was `undefined`. This behavior
|
|
401
|
+
* is the same as how `JSON.stringify` works so this is relatively expected
|
|
402
|
+
* behavior.
|
|
403
|
+
*/
|
|
260
404
|
function nullifyUndefined(item) {
|
|
261
|
-
|
|
405
|
+
return item === void 0 ? null : item;
|
|
262
406
|
}
|
|
263
|
-
function yes(
|
|
264
|
-
|
|
407
|
+
function yes() {
|
|
408
|
+
return !0;
|
|
265
409
|
}
|
|
266
|
-
export {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
diffValue
|
|
270
|
-
};
|
|
271
|
-
//# sourceMappingURL=index.js.map
|
|
410
|
+
export { DiffError, diffPatch, diffValue };
|
|
411
|
+
|
|
412
|
+
//# sourceMappingURL=index.js.map
|