Skip to content

Commit 39baafd

Browse files
authored
Add JSON1 support with backwards compatibility (#19)
1 parent 866d87d commit 39baafd

File tree

6 files changed

+1569
-40
lines changed

6 files changed

+1569
-40
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,23 @@ String insertion/deletion (i.e. `si`, `sd`) operations are generated for string
3232
> { p: [ 4 ], ld: 3 }
3333
>]
3434

35+
The [JSON1 OT Type](https://github.com/ottypes/json1) is supported as well. To generate ops for the JSON1 OT type, provide a reference to [diff-match-patch](https://github.com/google/diff-match-patch), [ot-json1](https://github.com/ottypes/json1) and [ot-text-unicode](https://github.com/ottypes/text-unicode).
36+
37+
var diffMatchPatch = require("diff-match-patch");
38+
var json1 = require("ot-json1");
39+
var textUnicode = require("ot-text-unicode");
40+
var diff = jsondiff(
41+
["foo", "The only change here is at the end.", 1, 2, 3],
42+
["foo", "The only change here is at the very end.", 1, 2],
43+
diffMatchPatch,
44+
json1,
45+
textUnicode
46+
);
47+
console.log(diff);
48+
49+
>[
50+
> [ 1, { "es": [ 31, "very " ] } ],
51+
> [ 4, { "r": true } ]
52+
>]
53+
3554
This was developed for [JsonML](http://www.jsonml.org/) with [Webstrates](https://github.com/cklokmose/Webstrates) in mind, but could be applicable in other situations.

index.js

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@
22

33
var equal = require("deep-equal");
44

5+
var diffMatchPatchInstance;
6+
7+
function replaceOp(path, isObject, input, output, json1) {
8+
var op;
9+
if (json1) {
10+
op = json1.replaceOp(path, input, output);
11+
} else {
12+
op = { p: path };
13+
op[isObject ? "od" : "ld"] = input;
14+
op[isObject ? "oi" : "li"] = output;
15+
}
16+
return [op];
17+
}
18+
519
/**
620
* Convert a number of string patches to OT operations.
721
* @param {JsonMLPath} path Base path for patches to apply to.
822
* @param {string} oldValue Old value.
923
* @param {string} newValue New value.
1024
* @return {Ops} List of resulting operations.
1125
*/
12-
function patchesToOps(path, oldValue, newValue, diffMatchPatch, diffMatchPatchInstance) {
26+
function patchesToOps(path, oldValue, newValue, diffMatchPatch, json1, textUnicode) {
1327
const ops = [];
1428

1529
var patches = diffMatchPatchInstance.patch_make(oldValue, newValue);
@@ -19,10 +33,20 @@ function patchesToOps(path, oldValue, newValue, diffMatchPatch, diffMatchPatchIn
1933
patch.diffs.forEach(function([type, value]) {
2034
switch (type) {
2135
case diffMatchPatch.DIFF_DELETE:
22-
ops.push({ sd: value, p: [...path, offset] });
36+
if (textUnicode) {
37+
var unicodeOp = textUnicode.remove(offset, value);
38+
ops.push(json1.editOp(path, textUnicode.type, unicodeOp));
39+
} else {
40+
ops.push({ sd: value, p: [...path, offset] });
41+
}
2342
break;
2443
case diffMatchPatch.DIFF_INSERT:
25-
ops.push({ si: value, p: [...path, offset] });
44+
if (textUnicode) {
45+
var unicodeOp = textUnicode.insert(offset, value);
46+
ops.push(json1.editOp(path, textUnicode.type, unicodeOp));
47+
} else {
48+
ops.push({ si: value, p: [...path, offset] });
49+
}
2650
// falls through intentionally
2751
case diffMatchPatch.DIFF_EQUAL:
2852
offset += value.length;
@@ -35,9 +59,11 @@ function patchesToOps(path, oldValue, newValue, diffMatchPatch, diffMatchPatchIn
3559
return ops;
3660
}
3761

38-
var diffMatchPatchInstance;
39-
40-
var optimize = function(ops) {
62+
var optimize = function(ops, options) {
63+
if (options && options.json1) {
64+
var compose = options.json1.type.compose;
65+
return ops.reduce(compose, null);
66+
}
4167
/*
4268
Optimization loop where we attempt to find operations that needlessly inserts and deletes identical objects right
4369
after each other, and then consolidate them.
@@ -76,27 +102,41 @@ var optimize = function(ops) {
76102
return ops;
77103
}
78104

79-
var diff = function(input, output, path=[], diffMatchPatch) {
105+
var diff = function(input, output, path=[], options) {
106+
var diffMatchPatch = options && options.diffMatchPatch;
107+
var json1 = options && options.json1;
108+
var textUnicode = options && options.textUnicode;
109+
80110
// If the last element of the path is a string, that means we're looking at a key, rather than
81111
// a number index. Objects use keys, so the target for our insertion/deletion is an object.
82112
var isObject = typeof path[path.length-1] === "string" || path.length === 0;
83113

84114
// If input and output are equal, no operations are needed.
85-
if (equal(input, output)) {
115+
if (equal(input, output) && Array.isArray(input) === Array.isArray(output)) {
86116
return [];
87117
}
88118

89119
// If there is no output, we need to delete the current data (input).
90120
if (typeof output === "undefined") {
91-
var op = { p: path };
92-
op[isObject ? "od" : "ld"] = input;
121+
var op;
122+
if (json1) {
123+
op = json1.removeOp(path, output);
124+
} else {
125+
op = { p: path };
126+
op[isObject ? "od" : "ld"] = input;
127+
}
93128
return [op];
94129
}
95130

96131
// If there is no input, we need to add the new data (output).
97132
if (typeof input === "undefined") {
98-
var op = { p: path };
99-
op[isObject ? "oi" : "li"] = output;
133+
var op;
134+
if (json1) {
135+
op = json1.insertOp(path, output);
136+
} else {
137+
op = { p: path };
138+
op[isObject ? "oi" : "li"] = output;
139+
}
100140
return [op];
101141
}
102142

@@ -108,17 +148,14 @@ var diff = function(input, output, path=[], diffMatchPatch) {
108148
diffMatchPatchInstance = new diffMatchPatch();
109149
}
110150

111-
return patchesToOps(path, input, output, diffMatchPatch, diffMatchPatchInstance);
151+
return patchesToOps(path, input, output, diffMatchPatch, json1, textUnicode);
112152
}
113153

114154
var primitiveTypes = ["string", "number", "boolean"];
115155
// If either of input/output is a primitive type, there is no need to perform deep recursive calls to
116156
// figure out what to do. We can just replace the objects.
117157
if (primitiveTypes.includes(typeof output) || primitiveTypes.includes(typeof input)) {
118-
var op = { p: path };
119-
op[isObject ? "od" : "ld"] = input;
120-
op[isObject ? "oi" : "li"] = output;
121-
return [op];
158+
return replaceOp(path, isObject, input, output, json1);
122159
}
123160

124161
if (Array.isArray(output) && Array.isArray(input)) {
@@ -127,23 +164,23 @@ var diff = function(input, output, path=[], diffMatchPatch) {
127164
var minLen = Math.min(inputLen, outputLen);
128165
var ops = [];
129166
for (var i=0; i < minLen; ++i) {
130-
var newOps = diff(input[i], output[i], [...path, i], diffMatchPatch);
167+
var newOps = diff(input[i], output[i], [...path, i], options);
131168
newOps.forEach(function(op) {
132169
ops.push(op);
133170
});
134171
}
135172
if (outputLen > inputLen) {
136173
// deal with array insert
137174
for (var i=minLen; i < outputLen; i++) {
138-
var newOps = diff(undefined, output[i], [...path, i], diffMatchPatch);
175+
var newOps = diff(undefined, output[i], [...path, i], options);
139176
newOps.forEach(function(op) {
140177
ops.push(op);
141178
});
142179
}
143180
} else if (outputLen < inputLen) {
144181
// deal with array delete
145182
for (var i=minLen; i < inputLen; i++) {
146-
var newOps = diff(input[i], undefined, [...path, minLen], diffMatchPatch);
183+
var newOps = diff(input[i], undefined, [...path, minLen], options);
147184
newOps.forEach(function(op) {
148185
ops.push(op);
149186
});
@@ -152,23 +189,21 @@ var diff = function(input, output, path=[], diffMatchPatch) {
152189
return ops;
153190
} else if (Array.isArray(output) || Array.isArray(input)) {
154191
// deal with array/object
155-
var op = { p: path };
156-
op[isObject ? "od" : "ld"] = input;
157-
op[isObject ? "oi" : "li"] = output;
158-
return [op];
192+
return replaceOp(path, isObject, input, output, json1);
159193
}
160194

161195
var ops = [];
162196
var keys = new Set([...Object.keys(input), ...Object.keys(output)]);
163197
keys.forEach(function(key) {
164-
var newOps = diff(input[key], output[key], [...path, key], diffMatchPatch);
198+
var newOps = diff(input[key], output[key], [...path, key], options);
165199
ops = ops.concat(newOps);
166200
});
167201
return ops;
168202
}
169203

170-
var optimizedDiff = function(input, output, diffMatchPatch) {
171-
return optimize(diff(input, output, [], diffMatchPatch));
204+
var optimizedDiff = function(input, output, diffMatchPatch, json1, textUnicode) {
205+
var options = { diffMatchPatch, json1, textUnicode };
206+
return optimize(diff(input, output, [], options), options);
172207
}
173208

174209
module.exports = optimizedDiff;

0 commit comments

Comments
 (0)