@sanity/diff-patch 5.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 +256 -96
- package/dist/index.d.ts +85 -222
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +400 -239
- package/dist/index.js.map +1 -1
- package/package.json +59 -76
- package/src/diffPatch.ts +454 -361
- package/src/index.ts +3 -15
- package/src/patches.ts +54 -33
- package/src/paths.ts +12 -4
- package/src/setOperations.ts +29 -0
- package/dist/index.cjs +0 -251
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -299
package/dist/index.js
CHANGED
|
@@ -1,251 +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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
84
|
+
}
|
|
85
|
+
function difference(source, target) {
|
|
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;
|
|
90
|
+
}
|
|
91
|
+
function intersection(source, target) {
|
|
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;
|
|
96
|
+
}
|
|
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
|
+
*/
|
|
119
|
+
function diffPatch(source, target, options = {}) {
|
|
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
|
+
} }));
|
|
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
|
+
*/
|
|
140
|
+
function diffValue(source, target, basePath = []) {
|
|
141
|
+
return serializePatches(diffItem(source, target, basePath));
|
|
142
|
+
}
|
|
143
|
+
function diffItem(source, target, path = [], patches = []) {
|
|
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);
|
|
152
|
+
}
|
|
153
|
+
function diffObject(source, target, path, patches) {
|
|
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;
|
|
167
|
+
}
|
|
168
|
+
function diffArray(source, target, path, patches) {
|
|
169
|
+
return isUniquelyKeyed(source) && isUniquelyKeyed(target) ? diffArrayByKey(source, target, path, patches) : diffArrayByIndex(source, target, path, patches);
|
|
170
|
+
}
|
|
171
|
+
function diffArrayByIndex(source, target, path, 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;
|
|
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
|
+
*/
|
|
211
|
+
function diffArrayByKey(source, target, path, patches) {
|
|
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;
|
|
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
|
+
*/
|
|
307
|
+
function shouldUseDiffMatchPatch(source, target) {
|
|
308
|
+
let maxLength = Math.max(source.length, target.length);
|
|
309
|
+
return maxLength > 1e6 ? !1 : maxLength < 1e4 || !(Math.abs(target.length - source.length) / maxLength > .4);
|
|
310
|
+
}
|
|
311
|
+
function getDiffMatchPatch(source, target, path) {
|
|
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
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function diffString(source, target, path, patches) {
|
|
324
|
+
let dmp = getDiffMatchPatch(source, target, path);
|
|
325
|
+
return patches.push(dmp ?? {
|
|
326
|
+
op: "set",
|
|
327
|
+
path,
|
|
328
|
+
value: target
|
|
329
|
+
}), patches;
|
|
174
330
|
}
|
|
175
331
|
function isNotIgnoredKey(key) {
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
function serializePatches(patches,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
332
|
+
return SYSTEM_KEYS.indexOf(key) === -1;
|
|
333
|
+
}
|
|
334
|
+
function serializePatches(patches, curr) {
|
|
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
|
+
}
|
|
208
380
|
}
|
|
209
381
|
function isUniquelyKeyed(arr) {
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return !!options.ifRevisionID;
|
|
242
|
-
}
|
|
243
|
-
function yes(_) {
|
|
244
|
-
return !0;
|
|
245
|
-
}
|
|
246
|
-
export {
|
|
247
|
-
DiffError,
|
|
248
|
-
diffItem,
|
|
249
|
-
diffPatch
|
|
250
|
-
};
|
|
251
|
-
//# sourceMappingURL=index.js.map
|
|
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;
|
|
388
|
+
}
|
|
389
|
+
const keyToIndexCache = /* @__PURE__ */ new WeakMap();
|
|
390
|
+
function getIndexForKey(keyedArray, 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];
|
|
395
|
+
}
|
|
396
|
+
function isRecord(value) {
|
|
397
|
+
return typeof value == "object" && !!value && !Array.isArray(value);
|
|
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
|
+
*/
|
|
404
|
+
function nullifyUndefined(item) {
|
|
405
|
+
return item === void 0 ? null : item;
|
|
406
|
+
}
|
|
407
|
+
function yes() {
|
|
408
|
+
return !0;
|
|
409
|
+
}
|
|
410
|
+
export { DiffError, diffPatch, diffValue };
|
|
411
|
+
|
|
412
|
+
//# sourceMappingURL=index.js.map
|