json-diff-react
Version:
A React.js component based on the json-diff package
291 lines (290 loc) • 12 kB
JavaScript
// This is copied from 'json-diff' package ('lib/index.js') with minor
// modifications.
import { extendedTypeOf, elisionMarker } from './utils';
import { SequenceMatcher } from '@ewoudenberg/difflib';
export default class JsonDiff {
constructor(options) {
var _a;
options.outputKeys = options.outputKeys || [];
options.excludeKeys = options.excludeKeys || [];
// Rendering ”...” elisions in the same way as for arrays
options.showElisionsForObjects = (_a = options.showElisionsForObjects) !== null && _a !== void 0 ? _a : true;
this.options = options;
}
isScalar(obj) {
return typeof obj !== 'object' || obj === null;
}
objectDiff(obj1, obj2) {
let result = {};
let score = 0;
let equal = true;
for (const [key, value] of Object.entries(obj1)) {
if (!this.options.outputNewOnly) {
const postfix = '__deleted';
if (!(key in obj2) && !this.options.excludeKeys.includes(key)) {
result[`${key}${postfix}`] = value;
score -= 30;
equal = false;
}
}
}
for (const [key, value] of Object.entries(obj2)) {
const postfix = !this.options.outputNewOnly ? '__added' : '';
if (!(key in obj1) && !this.options.excludeKeys.includes(key)) {
result[`${key}${postfix}`] = value;
score -= 30;
equal = false;
}
}
for (const [key, value1] of Object.entries(obj1)) {
if (key in obj2) {
if (this.options.excludeKeys.includes(key)) {
continue;
}
score += 20;
const value2 = obj2[key];
const change = this.diff(value1, value2);
if (!change.equal) {
result[key] = change.result;
equal = false;
}
else if (this.options.full || this.options.outputKeys.includes(key)) {
result[key] = value1;
}
else if (this.options.showElisionsForObjects) {
result[key] = elisionMarker;
}
// console.log(`key ${key} change.score=${change.score} ${change.result}`)
score += Math.min(20, Math.max(-10, change.score / 5)); // BATMAN!
}
}
if (equal) {
score = 100 * Math.max(Object.keys(obj1).length, 0.5);
if (!this.options.full) {
result = undefined;
}
}
else {
score = Math.max(0, score);
}
// console.log(`objectDiff(${JSON.stringify(obj1, null, 2)} <=> ${JSON.stringify(obj2, null, 2)}) == ${JSON.stringify({score, result, equal})}`)
return { score, result, equal };
}
findMatchingObject(item, index, fuzzyOriginals) {
// console.log("findMatchingObject: " + JSON.stringify({item, fuzzyOriginals}, null, 2))
let bestMatch = null;
for (const [key, { item: candidate, index: matchIndex }] of Object.entries(fuzzyOriginals)) {
if (key !== '__next') {
const indexDistance = Math.abs(matchIndex - index);
if (extendedTypeOf(item) === extendedTypeOf(candidate)) {
const { score } = this.diff(item, candidate);
if (!bestMatch ||
score > bestMatch.score ||
(score === bestMatch.score && indexDistance < bestMatch.indexDistance)) {
bestMatch = { score, key, indexDistance };
}
}
}
}
// console.log"findMatchingObject result = " + JSON.stringify(bestMatch, null, 2)
return bestMatch;
}
scalarize(array, originals, fuzzyOriginals) {
const fuzzyMatches = [];
if (fuzzyOriginals) {
// Find best fuzzy match for each object in the array
const keyScores = {};
for (let index = 0; index < array.length; index++) {
const item = array[index];
if (this.isScalar(item)) {
continue;
}
const bestMatch = this.findMatchingObject(item, index, fuzzyOriginals);
if (bestMatch &&
(!keyScores[bestMatch.key] || bestMatch.score > keyScores[bestMatch.key].score)) {
keyScores[bestMatch.key] = { score: bestMatch.score, index };
}
}
for (const [key, match] of Object.entries(keyScores)) {
fuzzyMatches[match.index] = key;
}
}
const result = [];
for (let index = 0; index < array.length; index++) {
const item = array[index];
if (this.isScalar(item)) {
result.push(item);
}
else {
const key = fuzzyMatches[index] || '__$!SCALAR' + originals.__next++;
originals[key] = { item, index };
result.push(key);
}
}
return result;
}
isScalarized(item, originals) {
return typeof item === 'string' && item in originals;
}
descalarize(item, originals) {
if (this.isScalarized(item, originals)) {
return originals[item].item;
}
else {
return item;
}
}
arrayDiff(obj1, obj2) {
const originals1 = { __next: 1 };
const seq1 = this.scalarize(obj1, originals1);
const originals2 = { __next: originals1.__next };
const seq2 = this.scalarize(obj2, originals2, originals1);
if (this.options.sort) {
seq1.sort();
seq2.sort();
}
const opcodes = new SequenceMatcher(null, seq1, seq2).getOpcodes();
// console.log(`arrayDiff:\nobj1 = ${JSON.stringify(obj1, null, 2)}\nobj2 = ${JSON.stringify(obj2, null, 2)}\nseq1 = ${JSON.stringify(seq1, null, 2)}\nseq2 = ${JSON.stringify(seq2, null, 2)}\nopcodes = ${JSON.stringify(opcodes, null, 2)}`)
let result = [];
let score = 0;
let equal = true;
for (const [op, i1, i2, j1, j2] of opcodes) {
let i, j;
let asc, end;
let asc1, end1;
let asc2, end2;
if (!(op === 'equal' || (this.options.keysOnly && op === 'replace'))) {
equal = false;
}
switch (op) {
case 'equal':
for (i = i1, end = i2, asc = i1 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
const item = seq1[i];
if (this.isScalarized(item, originals1)) {
if (!this.isScalarized(item, originals2)) {
throw new Error(`internal bug: isScalarized(item, originals1) != isScalarized(item, originals2) for item ${JSON.stringify(item)}`);
}
const item1 = this.descalarize(item, originals1);
const item2 = this.descalarize(item, originals2);
const change = this.diff(item1, item2);
if (!change.equal) {
result.push(['~', change.result]);
equal = false;
}
else {
if (this.options.full || this.options.keepUnchangedValues) {
result.push([' ', item1]);
}
else {
result.push([' ']);
}
}
}
else {
if (this.options.full || this.options.keepUnchangedValues) {
result.push([' ', item]);
}
else {
result.push([' ']);
}
}
score += 10;
}
break;
case 'delete':
for (i = i1, end1 = i2, asc1 = i1 <= end1; asc1 ? i < end1 : i > end1; asc1 ? i++ : i--) {
result.push(['-', this.descalarize(seq1[i], originals1)]);
score -= 5;
}
break;
case 'insert':
for (j = j1, end2 = j2, asc2 = j1 <= end2; asc2 ? j < end2 : j > end2; asc2 ? j++ : j--) {
result.push(['+', this.descalarize(seq2[j], originals2)]);
score -= 5;
}
break;
case 'replace':
if (!this.options.keysOnly) {
let asc3, end3;
let asc4, end4;
for (i = i1, end3 = i2, asc3 = i1 <= end3; asc3 ? i < end3 : i > end3; asc3 ? i++ : i--) {
result.push(['-', this.descalarize(seq1[i], originals1)]);
score -= 5;
}
for (j = j1, end4 = j2, asc4 = j1 <= end4; asc4 ? j < end4 : j > end4; asc4 ? j++ : j--) {
result.push(['+', this.descalarize(seq2[j], originals2)]);
score -= 5;
}
}
else {
let asc5, end5;
for (i = i1, end5 = i2, asc5 = i1 <= end5; asc5 ? i < end5 : i > end5; asc5 ? i++ : i--) {
const change = this.diff(this.descalarize(seq1[i], originals1), this.descalarize(seq2[i - i1 + j1], originals2));
if (!change.equal) {
result.push(['~', change.result]);
equal = false;
}
else {
result.push([' ']);
}
}
}
break;
}
}
if (equal || opcodes.length === 0) {
if (!this.options.full) {
result = undefined;
}
else {
result = obj1;
}
score = 100;
}
else {
score = Math.max(0, score);
}
return { score, result, equal };
}
diff(obj1, obj2) {
const type1 = extendedTypeOf(obj1);
const type2 = extendedTypeOf(obj2);
if (type1 === type2) {
switch (type1) {
case 'object':
return this.objectDiff(obj1, obj2);
case 'array':
return this.arrayDiff(obj1, obj2);
}
}
// Compare primitives or complex objects of different types
let score = 100;
let result = obj1;
let equal;
if (!this.options.keysOnly) {
if (type1 === 'date' && type2 === 'date') {
equal = obj1.getTime() === obj2.getTime();
}
else {
equal = obj1 === obj2;
}
if (!equal) {
score = 0;
if (this.options.outputNewOnly) {
result = obj2;
}
else {
result = { __old: obj1, __new: obj2 };
}
}
else if (!this.options.full) {
result = undefined;
}
}
else {
equal = true;
result = undefined;
}
return { score, result, equal };
}
}