@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/dist/index.js CHANGED
@@ -1,251 +1,412 @@
1
- import { makeDiff, cleanupEfficiency, 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" && 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;
47
- }
48
- const ignoredKeys = ["_id", "_type", "_createdAt", "_updatedAt", "_rev"], defaultOptions = {
49
- hideWarnings: !1,
50
- diffMatchPatch: {
51
- enabled: !0,
52
- // Only use diff-match-patch if target string is longer than this threshold
53
- lengthThresholdAbsolute: 30,
54
- // Only use generated diff-match-patch if the patch length is less than or equal to
55
- // (targetString * relative). Example: A 100 character target with a relative factor
56
- // of 1.2 will allow a 120 character diff-match-patch. If larger than this number,
57
- // it will fall back to a regular `set` patch.
58
- lengthThresholdRelative: 1.2
59
- }
60
- };
61
- function mergeOptions(options) {
62
- return {
63
- ...defaultOptions,
64
- ...options,
65
- diffMatchPatch: { ...defaultOptions.diffMatchPatch, ...options.diffMatchPatch || {} }
66
- };
67
- }
68
- function diffPatch(itemA, itemB, opts) {
69
- const options = mergeOptions(opts || {}), id = options.id || itemA._id === itemB._id && itemA._id, revisionLocked = options.ifRevisionID, ifRevisionID = typeof revisionLocked == "boolean" ? itemA._rev : revisionLocked, basePath = options.basePath || [];
70
- if (!id)
71
- throw new Error(
72
- "_id on itemA and itemB not present or differs, specify document id the mutations should be applied to"
73
- );
74
- if (revisionLocked === !0 && !ifRevisionID)
75
- throw new Error(
76
- "`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."
77
- );
78
- if (basePath.length === 0 && itemA._type !== itemB._type)
79
- throw new Error(`_type is immutable and cannot be changed (${itemA._type} => ${itemB._type})`);
80
- const operations = diffItem(itemA, itemB, options, basePath, []);
81
- return serializePatches(operations, { id, ifRevisionID: revisionLocked ? ifRevisionID : void 0 });
82
- }
83
- function diffItem(itemA, itemB, opts = defaultOptions, path = [], patches = []) {
84
- if (itemA === itemB)
85
- return patches;
86
- const aType = Array.isArray(itemA) ? "array" : typeof itemA, bType = Array.isArray(itemB) ? "array" : typeof itemB, aIsUndefined = aType === "undefined", bIsUndefined = bType === "undefined";
87
- if (aIsUndefined && !bIsUndefined)
88
- return patches.push({ op: "set", path, value: itemB }), patches;
89
- if (!aIsUndefined && bIsUndefined)
90
- return patches.push({ op: "unset", path }), patches;
91
- const options = mergeOptions(opts), dataType = aIsUndefined ? bType : aType;
92
- return dataType === "object" || dataType === "array" ? aType !== bType ? (patches.push({ op: "set", path, value: itemB }), patches) : dataType === "array" ? diffArray(itemA, itemB, options, path, patches) : diffObject(itemA, itemB, options, path, patches) : diffPrimitive(itemA, itemB, options, path, patches);
93
- }
94
- function diffObject(itemA, itemB, options, path, patches) {
95
- const atRoot = path.length === 0, aKeys = Object.keys(itemA).filter(atRoot ? isNotIgnoredKey : yes).map((key) => validateProperty(key, itemA[key], path)), aKeysLength = aKeys.length, bKeys = Object.keys(itemB).filter(atRoot ? isNotIgnoredKey : yes).map((key) => validateProperty(key, itemB[key], path)), bKeysLength = bKeys.length;
96
- for (let i = 0; i < aKeysLength; i++) {
97
- const key = aKeys[i];
98
- key in itemB || patches.push({ op: "unset", path: path.concat(key) });
99
- }
100
- for (let i = 0; i < bKeysLength; i++) {
101
- const key = bKeys[i];
102
- diffItem(itemA[key], itemB[key], options, path.concat([key]), patches);
103
- }
104
- return patches;
105
- }
106
- function diffArray(itemA, itemB, options, path, patches) {
107
- if (itemB.length > itemA.length && patches.push({
108
- op: "insert",
109
- after: path.concat([-1]),
110
- items: itemB.slice(itemA.length).map((item, i) => nullifyUndefined(item, path, i, options))
111
- }), itemB.length < itemA.length) {
112
- const isSingle = itemA.length - itemB.length === 1, unsetItems = itemA.slice(itemB.length);
113
- isRevisionLocked(options) || !isUniquelyKeyed(unsetItems) ? patches.push({
114
- op: "unset",
115
- path: path.concat([isSingle ? itemB.length : [itemB.length, ""]])
116
- }) : patches.push(
117
- ...unsetItems.map(
118
- (item) => ({ op: "unset", path: path.concat({ _key: item._key }) })
119
- )
120
- );
121
- }
122
- for (let i = 0; i < itemB.length; i++)
123
- if (Array.isArray(itemB[i]))
124
- throw new DiffError("Multi-dimensional arrays not supported", path.concat(i), itemB[i]);
125
- const overlapping = Math.min(itemA.length, itemB.length), segmentA = itemA.slice(0, overlapping), segmentB = itemB.slice(0, overlapping);
126
- return isUniquelyKeyed(segmentA) && isUniquelyKeyed(segmentB) ? diffArrayByKey(segmentA, segmentB, options, path, patches) : diffArrayByIndex(segmentA, segmentB, options, path, patches);
127
- }
128
- function diffArrayByIndex(itemA, itemB, options, path, patches) {
129
- for (let i = 0; i < itemA.length; i++)
130
- diffItem(
131
- itemA[i],
132
- nullifyUndefined(itemB[i], path, i, options),
133
- options,
134
- path.concat(i),
135
- patches
136
- );
137
- return patches;
138
- }
139
- function diffArrayByKey(itemA, itemB, options, path, patches) {
140
- const keyedA = indexByKey(itemA), keyedB = indexByKey(itemB);
141
- if (!arrayIsEqual(keyedA.keys, keyedB.keys))
142
- return diffArrayByIndex(itemA, itemB, options, path, patches);
143
- for (let i = 0; i < keyedB.keys.length; i++) {
144
- const key = keyedB.keys[i], valueA = keyedA.index[key], valueB = nullifyUndefined(keyedB.index[key], path, i, options);
145
- diffItem(valueA, valueB, options, path.concat({ _key: key }), patches);
146
- }
147
- return patches;
148
- }
149
- function getDiffMatchPatch(itemA, itemB, options, path) {
150
- const { enabled, lengthThresholdRelative, lengthThresholdAbsolute } = options.diffMatchPatch, segment = path[path.length - 1];
151
- if (!enabled || // Don't use for anything but strings
152
- typeof itemA != "string" || typeof itemB != "string" || // Don't use for `_key`, `_ref` etc
153
- typeof segment == "string" && segment[0] === "_" || // Don't use on short strings
154
- itemB.length < lengthThresholdAbsolute)
155
- return;
156
- let strPatch = "";
157
- try {
158
- const patch = makeDiff(itemA, itemB), diff = cleanupEfficiency(patch);
159
- strPatch = stringifyPatches(makePatches(diff));
160
- } catch {
161
- return;
162
- }
163
- return strPatch.length > itemB.length * lengthThresholdRelative ? void 0 : { op: "diffMatchPatch", path, value: strPatch };
164
- }
165
- function diffPrimitive(itemA, itemB, options, path, patches) {
166
- const dmp = getDiffMatchPatch(itemA, itemB, options, path);
167
- return patches.push(
168
- dmp || {
169
- op: "set",
170
- path,
171
- value: itemB
172
- }
173
- ), patches;
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
- return ignoredKeys.indexOf(key) === -1;
177
- }
178
- function serializePatches(patches, options) {
179
- if (patches.length === 0)
180
- return [];
181
- const { id, ifRevisionID } = options, set = patches.filter((patch) => patch.op === "set"), unset = patches.filter((patch) => patch.op === "unset"), insert = patches.filter((patch) => patch.op === "insert"), dmp = patches.filter((patch) => patch.op === "diffMatchPatch"), withSet = set.length > 0 && set.reduce(
182
- (patch, item) => {
183
- const path = pathToString(item.path);
184
- return patch.set[path] = item.value, patch;
185
- },
186
- { id, set: {} }
187
- ), withUnset = unset.length > 0 && unset.reduce(
188
- (patch, item) => {
189
- const path = pathToString(item.path);
190
- return patch.unset.push(path), patch;
191
- },
192
- { id, unset: [] }
193
- ), withInsert = insert.reduce((acc, item) => {
194
- const after = pathToString(item.after);
195
- return acc.concat({ id, insert: { after, items: item.items } });
196
- }, []), withDmp = dmp.length > 0 && dmp.reduce(
197
- (patch, item) => {
198
- const path = pathToString(item.path);
199
- return patch.diffMatchPatch[path] = item.value, patch;
200
- },
201
- { id, diffMatchPatch: {} }
202
- );
203
- return [withUnset, withSet, withDmp, ...withInsert].filter(
204
- (item) => item !== !1
205
- ).map((patch, i) => ({
206
- patch: ifRevisionID && i === 0 ? { ...patch, ifRevisionID } : patch
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
- const keys = [];
211
- for (let i = 0; i < arr.length; i++) {
212
- const key = getKey(arr[i]);
213
- if (!key || keys.indexOf(key) !== -1)
214
- return !1;
215
- keys.push(key);
216
- }
217
- return !0;
218
- }
219
- function getKey(obj) {
220
- return typeof obj == "object" && obj !== null && obj._key;
221
- }
222
- function indexByKey(arr) {
223
- return arr.reduce(
224
- (acc, item) => (acc.keys.push(item._key), acc.index[item._key] = item, acc),
225
- { keys: [], index: {} }
226
- );
227
- }
228
- function arrayIsEqual(itemA, itemB) {
229
- return itemA.length === itemB.length && itemA.every((item, i) => itemB[i] === item);
230
- }
231
- function nullifyUndefined(item, path, index, options) {
232
- if (typeof item < "u")
233
- return item;
234
- if (!options.hideWarnings) {
235
- const serializedPath = pathToString(path.concat(index));
236
- console.warn(`undefined value in array converted to null (at '${serializedPath}')`);
237
- }
238
- return null;
239
- }
240
- function isRevisionLocked(options) {
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