@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/dist/index.js CHANGED
@@ -1,271 +1,412 @@
1
- import { stringifyPatches, makePatches } from "@sanity/diff-match-patch";
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
- return path.reduce((target, segment, i) => {
5
- if (Array.isArray(segment))
6
- return `${target}[${segment.join(":")}]`;
7
- if (isKeyedObject(segment))
8
- return `${target}[_key=="${segment._key}"]`;
9
- if (typeof segment == "number")
10
- return `${target}[${segment}]`;
11
- if (typeof segment == "string" && !IS_DOTTABLE_RE.test(segment))
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
- return typeof obj == "object" && !!obj && "_key" in obj && typeof obj._key == "string";
21
+ return typeof obj == "object" && !!obj && "_key" in obj && typeof obj._key == "string";
20
22
  }
21
- class DiffError extends Error {
22
- path;
23
- value;
24
- serializedPath;
25
- constructor(message, path, value) {
26
- const serializedPath = pathToString(path);
27
- super(`${message} (at '${serializedPath}')`), this.path = path, this.serializedPath = serializedPath, this.value = value;
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
- if (!propStartPattern.test(property))
33
- throw new DiffError("Keys must start with a letter (a-z)", path.concat(property), value);
34
- if (!propPattern.test(property))
35
- throw new DiffError(
36
- "Keys can only contain letters, numbers and underscores",
37
- path.concat(property),
38
- value
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
- if ("difference" in Set.prototype)
50
- return source.difference(target);
51
- const result = /* @__PURE__ */ new Set();
52
- for (const item of source)
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
- if ("intersection" in Set.prototype)
58
- return source.intersection(target);
59
- const result = /* @__PURE__ */ new Set();
60
- for (const item of source)
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
- const SYSTEM_KEYS = ["_id", "_type", "_createdAt", "_updatedAt", "_rev"], DMP_MAX_STRING_SIZE = 1e6, DMP_MAX_STRING_LENGTH_CHANGE_RATIO = 0.4, DMP_MIN_SIZE_FOR_RATIO_CHECK = 1e4;
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
- const id = options.id || source._id === target._id && source._id, revisionLocked = options.ifRevisionID, ifRevisionID = typeof revisionLocked == "boolean" ? source._rev : revisionLocked, basePath = options.basePath || [];
67
- if (!id)
68
- throw new Error(
69
- "_id on source and target not present or differs, specify document id the mutations should be applied to"
70
- );
71
- if (revisionLocked === !0 && !ifRevisionID)
72
- throw new Error(
73
- "`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."
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
- return serializePatches(diffItem(source, target, basePath));
141
+ return serializePatches(diffItem(source, target, basePath));
89
142
  }
90
143
  function diffItem(source, target, path = [], patches = []) {
91
- 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({ op: "unset", path }), patches) : (patches.push({ op: "set", path, value: target }), 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);
92
152
  }
93
153
  function diffObject(source, target, path, patches) {
94
- const 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;
95
- for (let i = 0; i < aKeysLength; i++) {
96
- const key = aKeys[i];
97
- key in target || patches.push({ op: "unset", path: path.concat(key) });
98
- }
99
- for (let i = 0; i < bKeysLength; i++) {
100
- const key = bKeys[i];
101
- diffItem(source[key], target[key], path.concat([key]), patches);
102
- }
103
- return 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;
104
167
  }
105
168
  function diffArray(source, target, path, patches) {
106
- return isUniquelyKeyed(source) && isUniquelyKeyed(target) ? diffArrayByKey(source, target, path, patches) : diffArrayByIndex(source, target, path, patches);
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
- if (target.length > source.length && patches.push({
110
- op: "insert",
111
- position: "after",
112
- path: path.concat([-1]),
113
- items: target.slice(source.length).map(nullifyUndefined)
114
- }), target.length < source.length) {
115
- const isSingle = source.length - target.length === 1, unsetItems = source.slice(target.length);
116
- isUniquelyKeyed(unsetItems) ? patches.push(
117
- ...unsetItems.map(
118
- (item) => ({ op: "unset", path: path.concat({ _key: item._key }) })
119
- )
120
- ) : patches.push({
121
- op: "unset",
122
- path: path.concat([isSingle ? target.length : [target.length, ""]])
123
- });
124
- }
125
- for (let i = 0; i < target.length; i++)
126
- if (Array.isArray(target[i]))
127
- throw new DiffError("Multi-dimensional arrays not supported", path.concat(i), target[i]);
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
- const 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 = [];
135
- for (let i = 0; i < keysInBothArrays.size; i++) {
136
- const keyAtPositionInSource = sourceKeysStillPresent[i], keyAtPositionInTarget = targetKeysAlreadyPresent[i];
137
- keyAtPositionInSource !== keyAtPositionInTarget && keyReorderOperations.push({
138
- sourceKey: keyAtPositionInSource,
139
- targetKey: keyAtPositionInTarget
140
- });
141
- }
142
- keyReorderOperations.length && patches.push({
143
- op: "reorder",
144
- path,
145
- snapshot: source,
146
- reorders: keyReorderOperations
147
- });
148
- for (const key of keysInBothArrays)
149
- diffItem(sourceItemsByKey.get(key), targetItemsByKey.get(key), [...path, { _key: key }], patches);
150
- for (const keyToRemove of keysRemovedFromSource)
151
- patches.push({ op: "unset", path: [...path, { _key: keyToRemove }] });
152
- if (keysAddedToTarget.size) {
153
- let insertionAnchorKey, itemsPendingInsertion = [];
154
- const flushPendingInsertions = () => {
155
- itemsPendingInsertion.length && patches.push({
156
- op: "insert",
157
- // Insert after the anchor key if we have one, otherwise insert at the beginning
158
- ...insertionAnchorKey ? { position: "after", path: [...path, { _key: insertionAnchorKey }] } : { position: "before", path: [...path, 0] },
159
- items: itemsPendingInsertion
160
- });
161
- };
162
- for (const key of targetKeys)
163
- keysAddedToTarget.has(key) ? itemsPendingInsertion.push(targetItemsByKey.get(key)) : keysInBothArrays.has(key) && (flushPendingInsertions(), insertionAnchorKey = key, itemsPendingInsertion = []);
164
- flushPendingInsertions();
165
- }
166
- return 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;
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
- const maxLength = Math.max(source.length, target.length);
170
- return maxLength > DMP_MAX_STRING_SIZE ? !1 : maxLength < DMP_MIN_SIZE_FOR_RATIO_CHECK ? !0 : !(Math.abs(target.length - source.length) / maxLength > DMP_MAX_STRING_LENGTH_CHANGE_RATIO);
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
- const last = path.at(-1);
174
- if (!(typeof last == "string" && last.startsWith("_")) && shouldUseDiffMatchPatch(source, target))
175
- try {
176
- const strPatch = stringifyPatches(makePatches(source, target));
177
- return { op: "diffMatchPatch", path, value: strPatch };
178
- } catch {
179
- return;
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
- const dmp = getDiffMatchPatch(source, target, path);
184
- return patches.push(dmp ?? { op: "set", path, value: target }), patches;
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
- return SYSTEM_KEYS.indexOf(key) === -1;
332
+ return SYSTEM_KEYS.indexOf(key) === -1;
188
333
  }
189
334
  function serializePatches(patches, curr) {
190
- const [patch, ...rest] = patches;
191
- if (!patch) return curr ? [curr] : [];
192
- switch (patch.op) {
193
- case "set":
194
- case "diffMatchPatch": {
195
- const emptyOp = { [patch.op]: {} };
196
- 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);
197
- }
198
- case "unset": {
199
- const emptyOp = { unset: [] };
200
- return curr ? "unset" in curr ? (curr.unset.push(pathToString(patch.path)), serializePatches(rest, curr)) : [curr, ...serializePatches(patches, emptyOp)] : serializePatches(patches, emptyOp);
201
- }
202
- case "insert":
203
- return curr ? [curr, ...serializePatches(patches)] : [
204
- {
205
- insert: {
206
- [patch.position]: pathToString(patch.path),
207
- items: patch.items
208
- }
209
- },
210
- ...serializePatches(rest)
211
- ];
212
- case "reorder": {
213
- if (curr) return [curr, ...serializePatches(patches)];
214
- const tempKeyOperations = {};
215
- tempKeyOperations.set = {};
216
- for (const { sourceKey, targetKey } of patch.reorders) {
217
- const temporaryKey = `__temp_reorder_${sourceKey}__`, finalContentForThisPosition = patch.snapshot[getIndexForKey(patch.snapshot, targetKey)];
218
- Object.assign(tempKeyOperations.set, {
219
- [pathToString([...patch.path, { _key: sourceKey }])]: {
220
- ...finalContentForThisPosition,
221
- _key: temporaryKey
222
- }
223
- });
224
- }
225
- const finalKeyOperations = {};
226
- finalKeyOperations.set = {};
227
- for (const { sourceKey, targetKey } of patch.reorders) {
228
- const temporaryKey = `__temp_reorder_${sourceKey}__`;
229
- Object.assign(finalKeyOperations.set, {
230
- [pathToString([...patch.path, { _key: temporaryKey }, "_key"])]: targetKey
231
- });
232
- }
233
- return [tempKeyOperations, finalKeyOperations, ...serializePatches(rest)];
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
- const seenKeys = /* @__PURE__ */ new Set();
241
- for (const item of arr) {
242
- if (!isKeyedObject(item) || seenKeys.has(item._key)) return !1;
243
- seenKeys.add(item._key);
244
- }
245
- return !0;
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
- const cachedMapping = keyToIndexCache.get(keyedArray);
250
- if (cachedMapping) return cachedMapping[targetKey];
251
- const keyToIndexMapping = keyedArray.reduce(
252
- (mapping, { _key }, arrayIndex) => (mapping[_key] = arrayIndex, mapping),
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
- return typeof value == "object" && !!value && !Array.isArray(value);
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
- return item === void 0 ? null : item;
405
+ return item === void 0 ? null : item;
262
406
  }
263
- function yes(_) {
264
- return !0;
407
+ function yes() {
408
+ return !0;
265
409
  }
266
- export {
267
- DiffError,
268
- diffPatch,
269
- diffValue
270
- };
271
- //# sourceMappingURL=index.js.map
410
+ export { DiffError, diffPatch, diffValue };
411
+
412
+ //# sourceMappingURL=index.js.map