jsondiffpatch
Version:
JSON diff & patch (object and array diff, text diff, multiple output formats)
319 lines (318 loc) • 13 kB
JavaScript
import { assertArrayHasAtLeast2 } from "../assertions/arrays.js";
class BaseFormatter {
format(delta, left) {
const context = {};
this.prepareContext(context);
const preparedContext = context;
this.recurse(preparedContext, delta, left);
return this.finalize(preparedContext);
}
prepareContext(context) {
context.buffer = [];
context.out = function (...args) {
if (!this.buffer) {
throw new Error("context buffer is not initialized");
}
this.buffer.push(...args);
};
}
typeFormattterNotFound(_context, deltaType) {
throw new Error(`cannot format delta type: ${deltaType}`);
}
/* eslint-disable @typescript-eslint/no-unused-vars */
typeFormattterErrorFormatter(_context, _err, _delta, _leftValue, _key, _leftKey, _movedFrom) {
// do nothing by default
}
/* eslint-enable @typescript-eslint/no-unused-vars */
finalize({ buffer }) {
if (Array.isArray(buffer)) {
return buffer.join("");
}
return "";
}
recurse(context, delta, left, key, leftKey, movedFrom, isLast) {
const useMoveOriginHere = delta && movedFrom;
const leftValue = useMoveOriginHere ? movedFrom.value : left;
if (typeof delta === "undefined" && typeof key === "undefined") {
return undefined;
}
const type = this.getDeltaType(delta, movedFrom);
const nodeType = type === "node"
? delta._t === "a"
? "array"
: "object"
: "";
if (typeof key !== "undefined") {
this.nodeBegin(context, key, leftKey, type, nodeType, isLast !== null && isLast !== void 0 ? isLast : false);
}
else {
this.rootBegin(context, type, nodeType);
}
let typeFormattter;
try {
typeFormattter =
type !== "unknown"
? this[`format_${type}`]
: this.typeFormattterNotFound(context, type);
typeFormattter.call(this, context, delta, leftValue, key, leftKey, movedFrom);
}
catch (err) {
this.typeFormattterErrorFormatter(context, err, delta, leftValue, key, leftKey, movedFrom);
if (typeof console !== "undefined" && console.error) {
console.error(err.stack);
}
}
if (typeof key !== "undefined") {
this.nodeEnd(context, key, leftKey, type, nodeType, isLast !== null && isLast !== void 0 ? isLast : false);
}
else {
this.rootEnd(context, type, nodeType);
}
}
formatDeltaChildren(context, delta, left) {
this.forEachDeltaKey(delta, left, (key, leftKey, movedFrom, isLast) => {
this.recurse(context, delta[key], left ? left[leftKey] : undefined, key, leftKey, movedFrom, isLast);
});
}
forEachDeltaKey(delta, left, fn) {
const keys = [];
const arrayKeys = delta._t === "a";
if (!arrayKeys) {
// it's an object delta
const deltaKeys = Object.keys(delta);
// if left is provided, push all keys from it first, in the original order
if (typeof left === "object" && left !== null) {
keys.push(...Object.keys(left));
}
// then add new keys from delta, to the bottom
for (const key of deltaKeys) {
if (keys.indexOf(key) >= 0)
continue;
keys.push(key);
}
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
if (key === undefined)
continue;
const isLast = index === keys.length - 1;
fn(
// for object diff, the delta key and left key are the same
key, key,
// there's no "move" in object diff
undefined, isLast);
}
return;
}
// it's an array delta, this is a bit trickier because of position changes
const movedFrom = {};
for (const key in delta) {
if (Object.prototype.hasOwnProperty.call(delta, key)) {
const value = delta[key];
if (Array.isArray(value) && value[2] === 3) {
const movedDelta = value;
movedFrom[movedDelta[1]] = Number.parseInt(key.substring(1));
}
}
}
// go thru the array positions, finding delta keys on the way
const arrayDelta = delta;
let leftIndex = 0;
let rightIndex = 0;
const leftArray = Array.isArray(left) ? left : undefined;
const leftLength = leftArray
? leftArray.length
: // if we don't have the original array,
// use a length that ensures we'll go thru all delta keys
Object.keys(arrayDelta).reduce((max, key) => {
if (key === "_t")
return max;
const isLeftKey = key.substring(0, 1) === "_";
if (isLeftKey) {
const itemDelta = arrayDelta[key];
const leftIndex = Number.parseInt(key.substring(1));
const rightIndex = Array.isArray(itemDelta) &&
itemDelta.length >= 3 &&
itemDelta[2] === 3
? itemDelta[1]
: undefined;
const maxIndex = Math.max(leftIndex, rightIndex !== null && rightIndex !== void 0 ? rightIndex : 0);
return maxIndex > max ? maxIndex : max;
}
const rightIndex = Number.parseInt(key);
const leftIndex = movedFrom[rightIndex];
const maxIndex = Math.max(leftIndex !== null && leftIndex !== void 0 ? leftIndex : 0, rightIndex !== null && rightIndex !== void 0 ? rightIndex : 0);
return maxIndex > max ? maxIndex : max;
}, 0) + 1;
let rightLength = leftLength;
// call fn with previous args, to catch last call and set isLast=true
let previousFnArgs;
const addKey = (...args) => {
if (previousFnArgs) {
fn(...previousFnArgs);
}
previousFnArgs = args;
};
const flushLastKey = () => {
if (!previousFnArgs) {
return;
}
fn(previousFnArgs[0], previousFnArgs[1], previousFnArgs[2], true);
};
while (leftIndex < leftLength ||
rightIndex < rightLength ||
`${rightIndex}` in arrayDelta) {
let hasDelta = false;
const leftIndexKey = `_${leftIndex}`;
const rightIndexKey = `${rightIndex}`;
const movedFromIndex = rightIndex in movedFrom ? movedFrom[rightIndex] : undefined;
if (leftIndexKey in arrayDelta) {
// something happened to the left item at this position
hasDelta = true;
const itemDelta = arrayDelta[leftIndexKey];
addKey(leftIndexKey, movedFromIndex !== null && movedFromIndex !== void 0 ? movedFromIndex : leftIndex, movedFromIndex
? {
key: `_${movedFromIndex}`,
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined, false);
if (Array.isArray(itemDelta)) {
if (itemDelta[2] === 0) {
// deleted
rightLength--;
leftIndex++;
}
else if (itemDelta[2] === 3) {
// left item moved somewhere else
leftIndex++;
}
else {
// unrecognized change to left item
leftIndex++;
}
}
else {
// unrecognized change to left item
leftIndex++;
}
}
if (rightIndexKey in arrayDelta) {
// something happened to the right item at this position
hasDelta = true;
const itemDelta = arrayDelta[rightIndexKey];
const isItemAdded = Array.isArray(itemDelta) && itemDelta.length === 1;
addKey(rightIndexKey, movedFromIndex !== null && movedFromIndex !== void 0 ? movedFromIndex : leftIndex, movedFromIndex
? {
key: `_${movedFromIndex}`,
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined, false);
if (isItemAdded) {
// added
rightLength++;
rightIndex++;
}
else if (movedFromIndex === undefined) {
// modified (replace/object/array/textdiff)
leftIndex++;
rightIndex++;
}
else {
// move
rightIndex++;
}
}
if (!hasDelta) {
// left and right items are the same (unchanged)
if ((leftArray && movedFromIndex === undefined) ||
this.includeMoveDestinations !== false) {
// show unchanged items only if we have the left array
addKey(rightIndexKey, movedFromIndex !== null && movedFromIndex !== void 0 ? movedFromIndex : leftIndex, movedFromIndex
? {
key: `_${movedFromIndex}`,
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined, false);
}
if (movedFromIndex !== undefined) {
// item at the right came from another position
rightIndex++;
// don't skip left item yet
}
else {
leftIndex++;
rightIndex++;
}
}
}
flushLastKey();
}
getDeltaType(delta, movedFrom) {
if (typeof delta === "undefined") {
if (typeof movedFrom !== "undefined") {
return "movedestination";
}
return "unchanged";
}
if (Array.isArray(delta)) {
if (delta.length === 1) {
return "added";
}
if (delta.length === 2) {
return "modified";
}
if (delta.length === 3 && delta[2] === 0) {
return "deleted";
}
if (delta.length === 3 && delta[2] === 2) {
return "textdiff";
}
if (delta.length === 3 && delta[2] === 3) {
return "moved";
}
}
else if (typeof delta === "object") {
return "node";
}
return "unknown";
}
parseTextDiff(value) {
var _a;
const output = [];
const lines = value.split("\n@@ ");
for (const line of lines) {
const lineOutput = {
pieces: [],
};
const location = (_a = /^(?:@@ )?[-+]?(\d+),(\d+)/.exec(line)) === null || _a === void 0 ? void 0 : _a.slice(1);
if (!location) {
throw new Error("invalid text diff format");
}
assertArrayHasAtLeast2(location);
lineOutput.location = {
line: location[0],
chr: location[1],
};
const pieces = line.split("\n").slice(1);
for (let pieceIndex = 0, piecesLength = pieces.length; pieceIndex < piecesLength; pieceIndex++) {
const piece = pieces[pieceIndex];
if (piece === undefined || !piece.length) {
continue;
}
const pieceOutput = {
type: "context",
};
if (piece.substring(0, 1) === "+") {
pieceOutput.type = "added";
}
else if (piece.substring(0, 1) === "-") {
pieceOutput.type = "deleted";
}
pieceOutput.text = piece.slice(1);
lineOutput.pieces.push(pieceOutput);
}
output.push(lineOutput);
}
return output;
}
}
export default BaseFormatter;