Skip to content

Commit d5ee016

Browse files
committed
Add initial algorithm
1 parent 0c4b78f commit d5ee016

File tree

3 files changed

+1190
-4
lines changed

3 files changed

+1190
-4
lines changed

index.js

Lines changed: 328 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,332 @@
11
'use strict';
22

3+
var array = require('x-is-array');
4+
var object = require('is-object');
5+
var proto = require('getprototypeof');
6+
7+
var ignoredKeys = ['type', 'children', 'value'];
8+
39
module.exports = diff;
410

5-
function diff(left, right) {}
11+
function diff(left, right) {
12+
var patch = {left: left};
13+
walk(left, right, patch, 0);
14+
return patch;
15+
}
16+
17+
function walk(left, right, patch, index) {
18+
var apply = patch[index];
19+
20+
if (left === right) {
21+
return;
22+
}
23+
24+
if (!right) {
25+
apply = append(apply, {type: 'remove', left: left, right: null});
26+
} else if (!left) {
27+
apply = append(apply, {type: 'insert', left: null, right: right});
28+
} else if (left.type === right.type) {
29+
apply = diffProperties(apply, left, right);
30+
31+
/* Nodes of the same type must be of the same kind: if `right` is a
32+
* text, so is `left`. */
33+
if (text(right)) {
34+
apply = diffText(apply, left, right);
35+
} else if (parent(right)) {
36+
apply = diffChildren(apply, left, right, patch, index);
37+
}
38+
} else {
39+
apply = append(apply, {type: 'replace', left: left, right: right});
40+
}
41+
42+
if (apply) {
43+
patch[index] = apply;
44+
}
45+
}
46+
47+
function diffText(apply, left, right) {
48+
return left.value === right.value ?
49+
undefined :
50+
append(apply, {type: 'text', left: left, right: right});
51+
}
52+
53+
function diffChildren(apply, left, right, patch, offset) {
54+
var leftChildren = left.children;
55+
var ordered = reorder(leftChildren, right.children);
56+
var children = ordered.children;
57+
var length = children.length;
58+
var index = -1;
59+
var leftChild;
60+
var rightChild;
61+
62+
while (++index < length) {
63+
leftChild = leftChildren[index];
64+
rightChild = children[index];
65+
offset++;
66+
67+
if (leftChild) {
68+
walk(leftChild, rightChild, patch, offset);
69+
} else {
70+
/* Excess nodes in `right` need to be added */
71+
apply = append(apply, {type: 'insert', left: null, right: rightChild});
72+
}
73+
74+
offset += size(leftChild);
75+
}
76+
77+
if (ordered.moves) {
78+
/* Reorder nodes last */
79+
apply = append(apply, {type: 'order', left: left, right: ordered.moves});
80+
}
81+
82+
return apply;
83+
}
84+
85+
function diffProperties(apply, left, right) {
86+
var diff = diffObjects(left, right);
87+
return diff ? append(apply, {type: 'props', left: left, right: diff}) : undefined;
88+
}
89+
90+
function diffObjects(left, right) {
91+
var diff;
92+
var leftKey;
93+
var rightKey;
94+
var leftValue;
95+
var rightValue;
96+
var objectDiff;
97+
98+
for (leftKey in left) {
99+
if (ignoredKeys.indexOf(leftKey) !== -1) {
100+
continue;
101+
}
102+
103+
if (!(leftKey in right)) {
104+
diff = diff || {};
105+
diff[leftKey] = undefined;
106+
}
107+
108+
leftValue = left[leftKey];
109+
rightValue = right[leftKey];
110+
111+
if (leftValue === rightValue) {
112+
continue;
113+
}
114+
115+
if (object(leftValue) && object(rightValue)) {
116+
if (proto(rightValue) === proto(leftValue)) {
117+
objectDiff = diffObjects(leftValue, rightValue);
118+
119+
if (objectDiff) {
120+
diff = diff || {};
121+
diff[leftKey] = objectDiff;
122+
}
123+
} else {
124+
diff = diff || {};
125+
diff[leftKey] = rightValue;
126+
}
127+
} else {
128+
diff = diff || {};
129+
diff[leftKey] = rightValue;
130+
}
131+
}
132+
133+
for (rightKey in right) {
134+
if (ignoredKeys.indexOf(rightKey) !== -1) {
135+
continue;
136+
}
137+
138+
if (!(rightKey in left)) {
139+
diff = diff || {};
140+
diff[rightKey] = right[rightKey];
141+
}
142+
}
143+
144+
return diff;
145+
}
146+
147+
function append(apply, patch) {
148+
if (!apply) {
149+
return patch;
150+
}
151+
152+
if (array(apply)) {
153+
apply.push(patch);
154+
return apply;
155+
}
156+
157+
return [apply, patch];
158+
}
159+
160+
/* List diff, naive left to right reordering */
161+
function reorder(leftChildren, rightChildren) {
162+
var leftChildIndex = keyIndex(leftChildren);
163+
var leftKeys = leftChildIndex.key;
164+
var leftIndex = leftChildIndex.index;
165+
var rightChildIndex = keyIndex(rightChildren);
166+
var rightKeys = rightChildIndex.key;
167+
var rightIndex = rightChildIndex.index;
168+
var removes = [];
169+
var inserts = [];
170+
var leftLength = leftChildren.length;
171+
var rightLength = rightChildren.length;
172+
var leftOffset = 0;
173+
var rightOffset = 0;
174+
var leftKey = leftIndex[leftOffset];
175+
var rightKey = rightIndex[rightOffset];
176+
var leftMoved = [];
177+
var rightMoved = [];
178+
var leftDistance;
179+
var rightDistance;
180+
var index = -1;
181+
var next = [];
182+
var key;
183+
184+
while (++index < leftLength) {
185+
key = leftIndex[index];
186+
187+
if (key in rightKeys) {
188+
next.push(rightChildren[rightKeys[key]]);
189+
} else {
190+
next.push(null);
191+
}
192+
}
193+
194+
index = -1;
195+
196+
while (++index < rightLength) {
197+
key = rightIndex[index];
198+
199+
if (!(key in leftKeys)) {
200+
next.push(rightChildren[index]);
201+
}
202+
}
203+
204+
while (leftOffset < leftLength || rightOffset < rightLength) {
205+
/* The left node moved already. */
206+
if (leftMoved.indexOf(leftOffset) !== -1) {
207+
removes.push({
208+
from: leftOffset,
209+
left: leftChildren[leftOffset],
210+
right: rightChildren[rightKeys[leftKey]]
211+
});
212+
213+
leftKey = leftIndex[++leftOffset];
214+
/* The right node moved already. */
215+
} else if (rightMoved.indexOf(rightOffset) !== -1) {
216+
removes.push({
217+
from: rightOffset,
218+
left: leftChildren[leftKeys[rightKey]],
219+
right: rightChildren[rightOffset]
220+
});
221+
222+
rightKey = rightIndex[++rightOffset];
223+
} else if (!rightKey) {
224+
leftKey = leftIndex[++leftOffset];
225+
} else if (!leftKey) {
226+
rightKey = rightIndex[++rightOffset];
227+
} else if (!(leftKey in rightKeys)) {
228+
leftKey = leftIndex[++leftOffset];
229+
} else if (!(rightKey in leftKeys)) {
230+
rightKey = rightIndex[++rightOffset];
231+
} else if (rightKey === leftKey) {
232+
leftKey = leftIndex[++leftOffset];
233+
rightKey = rightIndex[++rightOffset];
234+
} else {
235+
leftDistance = leftKeys[rightKey] - leftOffset;
236+
rightDistance = rightKeys[leftKey] - rightOffset;
237+
238+
if (leftDistance > rightDistance) {
239+
inserts.push({
240+
to: rightOffset,
241+
left: leftChildren[leftKeys[rightKey]],
242+
right: rightChildren[rightOffset]
243+
});
244+
245+
leftMoved.push(leftKeys[rightKey]);
246+
rightKey = rightIndex[++rightOffset];
247+
} else {
248+
inserts.push({
249+
to: leftOffset,
250+
left: leftChildren[leftOffset],
251+
right: rightChildren[rightKeys[leftKey]]
252+
});
253+
254+
rightMoved.push(rightKeys[leftKey]);
255+
leftKey = leftIndex[++leftOffset];
256+
}
257+
}
258+
}
259+
260+
if (removes.length === 0 && inserts.length === 0) {
261+
return {children: next, moves: null};
262+
}
263+
264+
return {
265+
children: next,
266+
moves: {removes: removes, inserts: inserts}
267+
};
268+
}
269+
270+
function keyIndex(children) {
271+
var keys = {};
272+
var indices = [];
273+
var length = children.length;
274+
var counts = {};
275+
var index = -1;
276+
var child;
277+
var key;
278+
279+
while (++index < length) {
280+
child = children[index];
281+
key = syntheticKey(child);
282+
283+
if (key in counts) {
284+
key = key + ':' + counts[key]++;
285+
} else {
286+
counts[key] = 1;
287+
}
288+
289+
indices[index] = key;
290+
keys[key] = index;
291+
}
292+
293+
return {key: keys, index: indices};
294+
}
295+
296+
function syntheticKey(node) {
297+
var props = {};
298+
var key;
299+
300+
for (key in node) {
301+
if (ignoredKeys.indexOf(key) === -1) {
302+
props[key] = node[key];
303+
}
304+
}
305+
306+
return node.type + ':' + JSON.stringify(props);
307+
}
308+
309+
function size(node) {
310+
var children = node && node.children;
311+
var length = (children && children.length) || 0;
312+
var index = -1;
313+
var count = 0;
314+
315+
while (++index < length) {
316+
count = count + 1 + size(children[index]);
317+
}
318+
319+
return count;
320+
}
321+
322+
function parent(value) {
323+
return node(value) && 'children' in value;
324+
}
325+
326+
function text(value) {
327+
return node(value) && 'value' in value;
328+
}
329+
330+
function node(value) {
331+
return object(value) && 'type' in value;
332+
}

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
"contributors": [
1818
"Titus Wormer <[email protected]> (http://wooorm.com)"
1919
],
20-
"dependencies": {},
20+
"dependencies": {
21+
"getprototypeof": "^1.0.0",
22+
"is-object": "^1.0.1",
23+
"x-is-array": "^0.1.0"
24+
},
2125
"files": [
2226
"index.js"
2327
],
@@ -49,6 +53,9 @@
4953
},
5054
"xo": {
5155
"space": true,
56+
"rules": {
57+
"max-params": "off"
58+
},
5259
"ignores": [
5360
"unist-diff.js"
5461
]

0 commit comments

Comments
 (0)