(function(root, factory) {
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = factory();
} else {
exports.diffDOM = factory();
}
} else if (typeof define === 'function') {
// AMD loader
define(factory);
} else {
// `window` in the browser, or `exports` on the server
root.diffDOM = factory();
}
})(this, function() {
"use strict";
var diffcount, foundAll = false;
var Diff = function(options) {
var diff = this;
if (options) {
var keys = Object.keys(options),
length = keys.length,
i;
for (i = 0; i < length; i++) {
diff[keys[i]] = options[keys[i]];
}
}
};
Diff.prototype = {
toString: function() {
return JSON.stringify(this);
},
setValue: function(aKey, aValue) {
this[aKey] = aValue;
return this;
}
};
var elementDescriptors = function(el) {
var output = [];
if (el.nodeName !== '#text' && el.nodeName !== '#comment') {
output.push(el.nodeName);
if (el.attributes) {
if (el.attributes['class']) {
output.push(el.nodeName + '.' + el.attributes['class'].replace(/ /g, '.'));
}
if (el.attributes.id) {
output.push(el.nodeName + '#' + el.attributes.id);
}
}
}
return output;
};
var findUniqueDescriptors = function(li) {
var uniqueDescriptors = {},
duplicateDescriptors = {},
liLength = li.length,
nodeLength, node, descriptors, descriptor, inUnique, inDupes, i, j;
for (i = 0; i < liLength; i++) {
node = li[i];
nodeLength = node.length;
descriptors = elementDescriptors(node);
for (j = 0; j < nodeLength; j++) {
descriptor = descriptors[j];
inUnique = descriptor in uniqueDescriptors;
inDupes = descriptor in duplicateDescriptors;
if (!inUnique && !inDupes) {
uniqueDescriptors[descriptor] = true;
} else if (inUnique) {
delete uniqueDescriptors[descriptor];
duplicateDescriptors[descriptor] = true;
}
}
}
return uniqueDescriptors;
};
var uniqueInBoth = function(l1, l2) {
var l1Unique = findUniqueDescriptors(l1),
l2Unique = findUniqueDescriptors(l2),
inBoth = {},
keys = Object.keys(l1Unique),
length = keys.length,
key,
i;
for (i = 0; i < length; i++) {
key = keys[i];
if (l2Unique[key]) {
inBoth[key] = true;
}
}
return inBoth;
};
var removeDone = function(tree) {
delete tree.outerDone;
delete tree.innerDone;
delete tree.valueDone;
if (tree.childNodes) {
return tree.childNodes.every(removeDone);
} else {
return true;
}
};
var isEqual = function(e1, e2) {
var e1Attributes, e2Attributes;
if (!['nodeName', 'value', 'checked', 'selected', 'data'].every(function(element) {
if (e1[element] !== e2[element]) {
return false;
}
return true;
})) {
return false;
}
if (Boolean(e1.attributes) !== Boolean(e2.attributes)) {
return false;
}
if (Boolean(e1.childNodes) !== Boolean(e2.childNodes)) {
return false;
}
if (e1.attributes) {
e1Attributes = Object.keys(e1.attributes);
e2Attributes = Object.keys(e2.attributes);
if (e1Attributes.length !== e2Attributes.length) {
return false;
}
if (!e1Attributes.every(function(attribute) {
if (e1.attributes[attribute] !== e2.attributes[attribute]) {
return false;
}
return true;
})) {
return false;
}
}
if (e1.childNodes) {
if (e1.childNodes.length !== e2.childNodes.length) {
return false;
}
if (!e1.childNodes.every(function(childNode, index) {
return isEqual(childNode, e2.childNodes[index]);
})) {
return false;
}
}
return true;
};
var roughlyEqual = function(e1, e2, uniqueDescriptors, sameSiblings, preventRecursion) {
var childUniqueDescriptors, nodeList1, nodeList2;
if (!e1 || !e2) {
return false;
}
if (e1.nodeName !== e2.nodeName) {
return false;
}
if (e1.nodeName === '#text') {
// Note that we initially don't care what the text content of a node is,
// the mere fact that it's the same tag and "has text" means it's roughly
// equal, and then we can find out the true text difference later.
return preventRecursion ? true : e1.data === e2.data;
}
if (e1.nodeName in uniqueDescriptors) {
return true;
}
if (e1.attributes && e2.attributes) {
if (e1.attributes.id) {
if (e1.attributes.id !== e2.attributes.id) {
return false;
} else {
var idDescriptor = e1.nodeName + '#' + e1.attributes.id;
if (idDescriptor in uniqueDescriptors) {
return true;
}
}
}
if (e1.attributes['class'] && e1.attributes['class'] === e2.attributes['class']) {
var classDescriptor = e1.nodeName + '.' + e1.attributes['class'].replace(/ /g, '.');
if (classDescriptor in uniqueDescriptors) {
return true;
}
}
}
if (sameSiblings) {
return true;
}
nodeList1 = e1.childNodes ? e1.childNodes.slice().reverse() : [];
nodeList2 = e2.childNodes ? e2.childNodes.slice().reverse() : [];
if (nodeList1.length !== nodeList2.length) {
return false;
}
if (preventRecursion) {
return nodeList1.every(function(element, index) {
return element.nodeName === nodeList2[index].nodeName;
});
} else {
// note: we only allow one level of recursion at any depth. If 'preventRecursion'
// was not set, we must explicitly force it to true for child iterations.
childUniqueDescriptors = uniqueInBoth(nodeList1, nodeList2);
return nodeList1.every(function(element, index) {
return roughlyEqual(element, nodeList2[index], childUniqueDescriptors, true, true);
});
}
};
var cloneObj = function(obj) {
// TODO: Do we really need to clone here? Is it not enough to just return the original object?
return JSON.parse(JSON.stringify(obj));
};
/**
* based on https://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Longest_common_substring#JavaScript
*/
var findCommonSubsets = function(c1, c2, marked1, marked2) {
var lcsSize = 0,
index = [],
c1Length = c1.length,
c2Length = c2.length,
matches = Array.apply(null, new Array(c1Length + 1)).map(function() {
return [];
}), // set up the matching table
uniqueDescriptors = uniqueInBoth(c1, c2),
// If all of the elements are the same tag, id and class, then we can
// consider them roughly the same even if they have a different number of
// children. This will reduce removing and re-adding similar elements.
subsetsSame = c1Length === c2Length,
origin, ret, c1Index, c2Index, c1Element, c2Element;
if (subsetsSame) {
c1.some(function(element, i) {
var c1Desc = elementDescriptors(element),
c2Desc = elementDescriptors(c2[i]);
if (c1Desc.length !== c2Desc.length) {
subsetsSame = false;
return true;
}
c1Desc.some(function(description, i) {
if (description !== c2Desc[i]) {
subsetsSame = false;
return true;
}
});
if (!subsetsSame) {
return true;
}
});
}
// fill the matches with distance values
for (c1Index = 0; c1Index < c1Length; c1Index++) {
c1Element = c1[c1Index];
for (c2Index = 0; c2Index < c2Length; c2Index++) {
c2Element = c2[c2Index];
if (!marked1[c1Index] && !marked2[c2Index] && roughlyEqual(c1Element, c2Element, uniqueDescriptors, subsetsSame)) {
matches[c1Index + 1][c2Index + 1] = (matches[c1Index][c2Index] ? matches[c1Index][c2Index] + 1 : 1);
if (matches[c1Index + 1][c2Index + 1] >= lcsSize) {
lcsSize = matches[c1Index + 1][c2Index + 1];
index = [c1Index + 1, c2Index + 1];
}
} else {
matches[c1Index + 1][c2Index + 1] = 0;
}
}
}
if (lcsSize === 0) {
return false;
}
return {
oldValue: index[0] - lcsSize,
newValue: index[1] - lcsSize,
length: lcsSize
};
};
/**
* This should really be a predefined function in Array...
*/
var makeArray = function(n, v) {
return Array.apply(null, new Array(n)).map(function() {
return v;
});
};
/**
* Generate arrays that indicate which node belongs to which subset,
* or whether it's actually an orphan node, existing in only one
* of the two trees, rather than somewhere in both.
*
* So if t1 = ![]()