@sanity/diff
Version:
Generates diffs between documents and primitive types
645 lines (644 loc) • 18.9 kB
JavaScript
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, cleanupSemantic, makeDiff } from "@sanity/diff-match-patch";
function replaceProperty(parent, prop, value) {
return delete parent[prop], parent[prop] = value, value;
}
function getLongestCommonSubsequence(previous, next) {
return backtrack(getLengthMatrix(previous, next), previous, next);
}
function getLengthMatrix(previous, next) {
let len1 = previous.length, len2 = next.length, x = 0, y = 0, matrix = Array(len1 + 1);
for (x = 0; x < len1 + 1; x++) for (matrix[x] = [len2 + 1], y = 0; y < len2 + 1; y++) matrix[x][y] = 0;
for (x = 1; x < len1 + 1; x++) for (y = 1; y < len2 + 1; y++) previous[x - 1] === next[y - 1] ? matrix[x][y] = matrix[x - 1][y - 1] + 1 : matrix[x][y] = Math.max(matrix[x - 1][y], matrix[x][y - 1]);
return matrix;
}
function backtrack(matrix, previous, next) {
let prevIndex = previous.length, nextIndex = next.length, subsequence = {
sequence: [],
prevIndices: [],
nextIndices: []
};
for (; prevIndex !== 0 && nextIndex !== 0;) previous[prevIndex - 1] === next[nextIndex - 1] ? (subsequence.sequence.unshift(previous[prevIndex - 1]), subsequence.prevIndices.unshift(prevIndex - 1), subsequence.nextIndices.unshift(nextIndex - 1), --prevIndex, --nextIndex) : matrix[prevIndex][nextIndex - 1] > matrix[prevIndex - 1][nextIndex] ? --nextIndex : --prevIndex;
return subsequence;
}
function diffArray(fromInput, toInput, options) {
if (fromInput === toInput) return {
type: "array",
action: "unchanged",
isChanged: !1,
fromValue: fromInput.value,
toValue: toInput.value,
get items() {
let items = diffExactByPosition(fromInput, toInput, options);
if (!items) throw Error("invariant broken: equivalent input, but diff detected");
return replaceProperty(this, "items", items);
}
};
let keyedA = indexByKey(fromInput), keyedB = indexByKey(toInput);
if (keyedA && keyedB) return diffArrayByKey(fromInput, keyedA, toInput, keyedB, options);
let items = diffExactByPosition(fromInput, toInput, options);
return items ? buildArrayDiff(fromInput, toInput, items, !1) : diffArrayByReinsert(fromInput, toInput, options);
}
function buildArrayDiff(fromInput, toInput, items, isChanged) {
let fromValue = fromInput.value, toValue = toInput.value;
return isChanged ? {
type: "array",
action: "changed",
isChanged: !0,
fromValue,
toValue,
items,
annotation: toInput.annotation
} : {
type: "array",
action: "unchanged",
isChanged: !1,
fromValue,
toValue,
items
};
}
/**
* Diffes the two arrays by position. Returns an `items` array if they are unchanged, or undefined
* if there are any changes anywhere.
*/
function diffExactByPosition(fromInput, toInput, options) {
if (fromInput.length !== toInput.length) return;
let items = [];
for (let idx = 0; idx < fromInput.length; idx++) {
let diff = diffInput(fromInput.at(idx), toInput.at(idx), options);
if (diff.isChanged) return;
items.push({
fromIndex: idx,
toIndex: idx,
hasMoved: !1,
diff,
annotation: toInput.annotationAt(idx)
});
}
return items;
}
function diffArrayByReinsert(fromInput, toInput, options) {
let items = [];
for (let idx = 0; idx < toInput.length; idx++) {
let input = toInput.at(idx);
items.push({
fromIndex: void 0,
toIndex: idx,
hasMoved: !1,
diff: addedInput(input, void 0, options),
annotation: input.annotation
});
}
for (let idx = 0; idx < fromInput.length; idx++) {
let input = fromInput.at(idx);
items.push({
fromIndex: idx,
toIndex: void 0,
hasMoved: !1,
diff: removedInput(input, void 0, options),
annotation: input.annotation
});
}
return buildArrayDiff(fromInput, toInput, items, !0);
}
/**
* Diff an array when all the elements have _key in the same position.
*/
function diffArrayByKey(fromArray, fromKeyIndex, toArray, toKeyIndex, options) {
let items = [], isChanged = !1;
function diffCommon(key, fromIndex, toIndex, hasMoved) {
deletePositionInIndex(fromKeyIndex.index, key, fromIndex), deletePositionInIndex(toKeyIndex.index, key, toIndex);
let diff = diffInput(fromArray.at(fromIndex), toArray.at(toIndex));
items.push({
fromIndex,
toIndex,
hasMoved,
diff,
annotation: toArray.annotationAt(toIndex)
}), (diff.isChanged || fromIndex !== toIndex) && (isChanged = !0);
}
let lcs = getLongestCommonSubsequence(fromKeyIndex.keys, toKeyIndex.keys);
for (let fromIndex = 0; fromIndex < fromKeyIndex.keys.length; fromIndex++) {
let key = fromKeyIndex.keys[fromIndex], subsequenceIdx = lcs.prevIndices.indexOf(fromIndex);
if (subsequenceIdx !== -1) {
diffCommon(key, fromIndex, lcs.nextIndices[subsequenceIdx], !1);
continue;
}
let toIndexes = toKeyIndex.index.get(key), toIndex = toIndexes && toIndexes.find((idx) => !lcs.nextIndices.includes(idx));
if (toIndex !== void 0) {
diffCommon(key, fromIndex, toIndex, !0);
continue;
}
let input = fromArray.at(fromIndex);
items.push({
fromIndex,
toIndex: void 0,
hasMoved: !1,
diff: removedInput(input, void 0, options),
annotation: fromArray.annotationAt(fromIndex)
}), isChanged = !0;
}
for (let positions of toKeyIndex.index.values()) {
for (let toIndex of positions) {
let input = toArray.at(toIndex);
items.push({
fromIndex: void 0,
toIndex,
hasMoved: !1,
diff: addedInput(input, void 0, options),
annotation: toArray.annotationAt(toIndex)
});
}
isChanged = !0;
}
return items.sort(compareItemDiff), buildArrayDiff(fromArray, toArray, items, isChanged);
}
function compareItemDiff(a, b) {
if (a.toIndex !== void 0 && b.toIndex !== void 0) return a.toIndex - b.toIndex;
if (a.fromIndex !== void 0 && b.fromIndex !== void 0) return a.fromIndex - b.fromIndex;
if (a.fromIndex !== void 0 && b.toIndex !== void 0) return -1;
if (a.toIndex !== void 0 && b.fromIndex !== void 0) return 1;
throw Error("invalid item diff comparison");
}
function deletePositionInIndex(index, key, pos) {
let positions = index.get(key);
deleteArrayValue(positions, pos), positions.length === 0 && index.delete(key);
}
function deleteArrayValue(arr, value) {
let idx = arr.indexOf(value);
if (idx === -1) throw Error("value not found");
arr.splice(idx, 1);
}
/**
* Indexes the array by a key. This handles cases where the items are:
*
* - Objects with _key
* - Strings
* - Numbers
*/
function indexByKey(arr) {
let index = /* @__PURE__ */ new Map(), keys = [], length = arr.length;
for (let i = 0; i < length; i++) {
let item = arr.at(i), key = null;
switch (item.type) {
case "string":
key = `s${item.value}`;
break;
case "number":
key = item.value;
break;
case "boolean":
key = item.value;
break;
case "null":
key = "n";
break;
case "object":
{
let keyField = item.get("_key");
if (keyField && keyField.type === "string" && (key = `k${keyField.value}`, index.has(key))) return;
}
break;
default:
}
if (key === null) return;
keys.push(key);
let positions = index.get(key);
positions || (positions = [], index.set(key, positions)), positions.push(i);
}
return {
keys,
index
};
}
function removedArray(input, toValue, options) {
return {
type: "array",
action: "removed",
isChanged: !0,
fromValue: input.value,
toValue,
annotation: input.annotation,
get items() {
let items = [];
for (let i = 0; i < input.length; i++) {
let item = input.at(i);
items.push({
fromIndex: i,
toIndex: void 0,
hasMoved: !1,
diff: removedInput(item, void 0, options),
annotation: input.annotationAt(i)
});
}
return replaceProperty(this, "items", items);
}
};
}
function addedArray(input, fromValue, options) {
return {
type: "array",
action: "added",
isChanged: !0,
fromValue,
toValue: input.value,
annotation: input.annotation,
get items() {
let items = [];
for (let i = 0; i < input.length; i++) {
let item = input.at(i);
items.push({
fromIndex: void 0,
toIndex: i,
hasMoved: !1,
diff: addedInput(item, void 0, options),
annotation: input.annotationAt(i)
});
}
return replaceProperty(this, "items", items);
}
};
}
const ignoredFields = /* @__PURE__ */ new Set([
"_id",
"_type",
"_createdAt",
"_updatedAt",
"_rev",
"_weak"
]);
function diffObject(fromInput, toInput, options) {
let fields = {}, isChanged = !1;
for (let key of fromInput.keys) {
if (ignoredFields.has(key)) continue;
let fromField = fromInput.get(key), toField = toInput.get(key);
if (toField) {
let fieldDiff = diffInput(fromField, toField, options);
fields[key] = fieldDiff, fieldDiff.isChanged && (isChanged = !0);
} else fields[key] = removedInput(fromField, void 0, options), isChanged = !0;
}
for (let key of toInput.keys) ignoredFields.has(key) || fromInput.get(key) || (fields[key] = addedInput(toInput.get(key), void 0, options), isChanged = !0);
let fromValue = fromInput.value, toValue = toInput.value;
return isChanged ? {
type: "object",
action: "changed",
isChanged: !0,
fromValue,
toValue,
fields,
annotation: toInput.annotation
} : {
type: "object",
action: "unchanged",
isChanged: !1,
fromValue,
toValue,
fields
};
}
function removedObject(input, toValue, options) {
return {
type: "object",
action: "removed",
isChanged: !0,
fromValue: input.value,
toValue,
annotation: input.annotation,
get fields() {
let fields = {};
for (let key of input.keys) fields[key] = removedInput(input.get(key), void 0, options);
return replaceProperty(this, "fields", fields);
}
};
}
function addedObject(input, fromValue, options) {
return {
type: "object",
action: "added",
isChanged: !0,
fromValue,
toValue: input.value,
annotation: input.annotation,
get fields() {
let fields = {};
for (let key of input.keys) fields[key] = addedInput(input.get(key), void 0, options);
return replaceProperty(this, "fields", fields);
}
};
}
function diffNumber(fromInput, toInput, options) {
let fromValue = fromInput.value, toValue = toInput.value, type = fromInput.type;
return fromValue === toValue ? {
type,
action: "unchanged",
fromValue,
toValue,
isChanged: !1
} : {
type: fromInput.type,
action: "changed",
isChanged: !0,
fromValue,
toValue,
annotation: toInput.annotation
};
}
function diffBoolean(fromInput, toInput, options) {
let fromValue = fromInput.value, toValue = toInput.value, type = fromInput.type;
return fromValue === toValue ? {
type,
action: "unchanged",
fromValue,
toValue,
isChanged: !1
} : {
type: fromInput.type,
action: "changed",
isChanged: !0,
fromValue,
toValue,
annotation: toInput.annotation
};
}
function diffString(fromInput, toInput, options) {
let fromValue = fromInput.value, toValue = toInput.value;
return fromValue === toValue ? {
type: "string",
action: "unchanged",
isChanged: !1,
fromValue,
toValue,
segments: [{
type: "stringSegment",
action: "unchanged",
text: fromValue
}]
} : {
type: "string",
action: "changed",
isChanged: !0,
fromValue,
toValue,
annotation: toInput.annotation,
get segments() {
let segments = buildSegments(fromInput, toInput);
return replaceProperty(this, "segments", segments);
}
};
}
function buildSegments(fromInput, toInput) {
let segments = [], dmpDiffs = cleanupSemantic(makeDiff(fromInput.value, toInput.value)), fromIdx = 0, toIdx = 0;
for (let [op, text] of dmpDiffs) switch (op) {
case DIFF_EQUAL:
segments.push({
type: "stringSegment",
action: "unchanged",
text
}), fromIdx += text.length, toIdx += text.length;
break;
case DIFF_DELETE:
for (let segment of fromInput.sliceAnnotation(fromIdx, fromIdx + text.length)) segments.push({
type: "stringSegment",
action: "removed",
text: segment.text,
annotation: segment.annotation
});
fromIdx += text.length;
break;
case DIFF_INSERT:
for (let segment of toInput.sliceAnnotation(toIdx, toIdx + text.length)) segments.push({
type: "stringSegment",
action: "added",
text: segment.text,
annotation: segment.annotation
});
toIdx += text.length;
break;
default: throw Error(`Unhandled diff-match-patch operation "${op}"`);
}
return segments;
}
function removedString(input, toValue, options) {
return {
type: "string",
action: "removed",
isChanged: !0,
fromValue: input.value,
toValue,
annotation: input.annotation,
get segments() {
let segments = input.sliceAnnotation(0, input.value.length).map((segment) => ({
type: "stringSegment",
action: "removed",
...segment
}));
return replaceProperty(this, "segments", segments);
}
};
}
function addedString(input, fromValue, options) {
return {
type: "string",
action: "added",
isChanged: !0,
fromValue,
toValue: input.value,
annotation: input.annotation,
get segments() {
let segments = input.sliceAnnotation(0, input.value.length).map((segment) => ({
type: "stringSegment",
action: "added",
...segment
}));
return replaceProperty(this, "segments", segments);
}
};
}
function diffTypeChange(fromInput, toInput, options) {
return {
type: "typeChange",
action: "changed",
isChanged: !0,
fromType: fromInput.type,
fromValue: fromInput.value,
fromDiff: removedInput(fromInput, void 0, options),
toType: toInput.type,
toValue: toInput.value,
toDiff: addedInput(toInput, void 0, options),
annotation: toInput.annotation
};
}
/**
* Takes a `from` and `to` input and calulates a diff between the two
*
* @param fromInput - The source (`from`) input - use {@link wrap | the wrap() method} to generate an "input"
* @param toInput - The destination (`to`) input - use {@link wrap | the wrap() method} to generate an "input"
* @param options - Options for the diffing process - currently no options are defined
* @returns A diff object representing the change
* @public
*/
function diffInput(fromInput, toInput, options = {}) {
return fromInput.type === toInput.type ? diffWithType(fromInput.type, fromInput, toInput, options) : fromInput.type === "null" ? addedInput(toInput, null, options) : toInput.type === "null" ? removedInput(fromInput, null, options) : diffTypeChange(fromInput, toInput, options);
}
function diffWithType(type, fromInput, toInput, options) {
switch (type) {
case "null": return {
type: "null",
action: "unchanged",
isChanged: !1,
toValue: null,
fromValue: null
};
case "boolean": return diffBoolean(fromInput, toInput, options);
case "number": return diffNumber(fromInput, toInput, options);
case "string": return diffString(fromInput, toInput, options);
case "array": return diffArray(fromInput, toInput, options);
case "object": return diffObject(fromInput, toInput, options);
default: throw Error(`Unhandled diff type "${type}"`);
}
}
function removedInput(input, toValue, options) {
switch (input.type) {
case "null": return {
type: "null",
action: "removed",
isChanged: !0,
fromValue: null,
toValue,
annotation: input.annotation
};
case "boolean": return {
type: "boolean",
action: "removed",
isChanged: !0,
fromValue: input.value,
toValue,
annotation: input.annotation
};
case "number": return {
type: "number",
action: "removed",
isChanged: !0,
fromValue: input.value,
toValue,
annotation: input.annotation
};
case "string": return removedString(input, toValue, options);
case "array": return removedArray(input, toValue, options);
case "object": return removedObject(input, toValue, options);
default: throw Error("Unhandled diff type");
}
}
function addedInput(input, fromValue, options) {
switch (input.type) {
case "null": return {
type: "null",
action: "added",
isChanged: !0,
fromValue,
toValue: null,
annotation: input.annotation
};
case "boolean": return {
type: "boolean",
action: "added",
isChanged: !0,
fromValue,
toValue: input.value,
annotation: input.annotation
};
case "number": return {
type: "number",
action: "added",
isChanged: !0,
fromValue,
toValue: input.value,
annotation: input.annotation
};
case "string": return addedString(input, fromValue, options);
case "array": return addedArray(input, fromValue, options);
case "object": return addedObject(input, fromValue, options);
default: throw Error("Unhandled diff type");
}
}
var ArrayWrapper = class {
type = "array";
length;
value;
annotation;
elements = [];
constructor(value, annotation) {
this.annotation = annotation, this.value = value, this.length = value.length;
}
at(idx) {
if (idx >= this.length) throw Error("out of bounds");
return this.elements[idx] || (this.elements[idx] = wrap(this.value[idx], this.annotation));
}
annotationAt() {
return this.annotation;
}
}, BasicWrapper = class {
type;
value;
annotation;
constructor(type, value, annotation) {
this.type = type, this.value = value, this.annotation = annotation;
}
}, ObjectWrapper = class {
type = "object";
value;
keys;
annotation;
fields = {};
constructor(value, annotation) {
this.value = filterUndefinedEntries(value), this.annotation = annotation, this.keys = Object.keys(this.value);
}
get(key) {
let input = this.fields[key];
if (input) return input;
if (!this.value.hasOwnProperty(key)) return;
let raw = this.value[key];
return this.fields[key] = wrap(raw, this.annotation);
}
};
function filterUndefinedEntries(value) {
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== void 0));
}
var StringWrapper = class {
type = "string";
value;
annotation;
constructor(value, annotation) {
this.value = value, this.annotation = annotation;
}
sliceAnnotation(start, end) {
return [{
text: this.value.slice(start, end),
annotation: this.annotation
}];
}
};
/**
* Takes an input (any JSON-serializable value) and an annotation, and generates an input
* object for it, to be used with {@link diffInput | the diffInput() method} and others.
*
* @param input - The value to wrap in an input object
* @param annotation - Annotation attached to the input - will be bound to generated diffs
* @returns A input object
* @throws if `input` is not a JSON-serializable type
* @public
*/
function wrap(input, annotation) {
if (Array.isArray(input)) return new ArrayWrapper(input, annotation);
if (input === null) return new BasicWrapper("null", input, annotation);
let type = typeof input;
switch (type) {
case "number": return new BasicWrapper(type, input, annotation);
case "boolean": return new BasicWrapper(type, input, annotation);
case "object": return new ObjectWrapper(input, annotation);
case "string": return new StringWrapper(input, annotation);
default: throw Error(`cannot wrap value of type: ${type}`);
}
}
export { diffInput, wrap };
//# sourceMappingURL=index.js.map