UNPKG

visual-dom-diff

Version:

Highlight differences between two DOM trees.

491 lines (490 loc) 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var diff_match_patch_1 = require("diff-match-patch"); var config_1 = require("./config"); var domIterator_1 = require("./domIterator"); var util_1 = require("./util"); /** * A simple helper which allows us to treat TH as TD in certain situations. */ var nodeNameOverride = function (nodeName) { return nodeName === 'TH' ? 'TD' : nodeName; }; /** * Stringifies a DOM node recursively. Text nodes are represented by their `data`, * while all other nodes are represented by a single Unicode code point * from the Private Use Area of the Basic Multilingual Plane. */ var serialize = function (root, config) { return new domIterator_1.DomIterator(root, config).reduce(function (text, node) { return text + (util_1.isText(node) ? node.data : util_1.charForNodeName(nodeNameOverride(node.nodeName))); }, ''); }; var getLength = function (node) { return (util_1.isText(node) ? node.length : 1); }; var isTr = function (node) { return node.nodeName === 'TR'; }; var isNotTr = function (node) { return !isTr(node); }; var trIteratorOptions = { skipChildren: isTr, skipSelf: isNotTr, }; function visualDomDiff(oldRootNode, newRootNode, options) { var _a, _b; if (options === void 0) { options = {}; } // Define config and simple helpers. var document = newRootNode.ownerDocument || newRootNode; var config = config_1.optionsToConfig(options); var addedClass = config.addedClass, diffText = config.diffText, modifiedClass = config.modifiedClass, removedClass = config.removedClass, skipSelf = config.skipSelf, skipChildren = config.skipChildren; var notSkipSelf = function (node) { return !skipSelf(node); }; var getDepth = function (node, rootNode) { return util_1.getAncestors(node, rootNode).filter(notSkipSelf).length; }; var isFormattingNode = function (node) { return util_1.isElement(node) && skipSelf(node); }; var getFormattingAncestors = function (node, rootNode) { return util_1.getAncestors(node, rootNode) .filter(isFormattingNode) .reverse(); }; var getColumnValue = function (node) { return addedNodes.has(node) ? 1 : removedNodes.has(node) ? -1 : 0; }; // Input iterators. var diffArray = diffText(serialize(oldRootNode, config), serialize(newRootNode, config)); var diffIndex = 0; var oldIterator = new domIterator_1.DomIterator(oldRootNode, config); var newIterator = new domIterator_1.DomIterator(newRootNode, config); // Input variables produced by the input iterators. var oldDone; var newDone; var diffItem; var oldNode; var newNode; var diffOffset = 0; var oldOffset = 0; var newOffset = 0; diffItem = diffArray[diffIndex++]; (_a = oldIterator.next(), oldDone = _a.done, oldNode = _a.value); (_b = newIterator.next(), newDone = _b.done, newNode = _b.value); // Output variables. var rootOutputNode = document.createDocumentFragment(); var oldOutputNode = rootOutputNode; var oldOutputDepth = 0; var newOutputNode = rootOutputNode; var newOutputDepth = 0; var removedNode = null; var addedNode = null; var removedNodes = new Set(); var addedNodes = new Set(); var modifiedNodes = new Set(); var formattingMap = new Map(); var equalTables = new Array(); var equalRows = new Map(); function prepareOldOutput() { var depth = getDepth(oldNode, oldRootNode); while (oldOutputDepth > depth) { /* istanbul ignore if */ if (!oldOutputNode.parentNode) { return util_1.never(); } if (oldOutputNode === removedNode) { removedNode = null; } oldOutputNode = oldOutputNode.parentNode; oldOutputDepth--; } /* istanbul ignore if */ if (oldOutputDepth !== depth) { return util_1.never(); } } function prepareNewOutput() { var depth = getDepth(newNode, newRootNode); while (newOutputDepth > depth) { /* istanbul ignore if */ if (!newOutputNode.parentNode) { return util_1.never(); } if (newOutputNode === addedNode) { addedNode = null; } newOutputNode = newOutputNode.parentNode; newOutputDepth--; } /* istanbul ignore if */ if (newOutputDepth !== depth) { return util_1.never(); } } function appendCommonChild(node) { /* istanbul ignore if */ if (oldOutputNode !== newOutputNode || addedNode || removedNode) { return util_1.never(); } if (util_1.isText(node)) { var oldFormatting = getFormattingAncestors(oldNode, oldRootNode); var newFormatting = getFormattingAncestors(newNode, newRootNode); formattingMap.set(node, newFormatting); var length_1 = oldFormatting.length; if (length_1 !== newFormatting.length) { modifiedNodes.add(node); } else { for (var i = 0; i < length_1; ++i) { if (!util_1.areNodesEqual(oldFormatting[i], newFormatting[i])) { modifiedNodes.add(node); break; } } } } else { if (!util_1.areNodesEqual(oldNode, newNode)) { modifiedNodes.add(node); } var nodeName = oldNode.nodeName; if (nodeName === 'TABLE') { equalTables.push({ newTable: newNode, oldTable: oldNode, outputTable: node, }); } else if (nodeName === 'TR') { equalRows.set(node, { newRow: newNode, oldRow: oldNode, }); } } newOutputNode.appendChild(node); oldOutputNode = node; newOutputNode = node; oldOutputDepth++; newOutputDepth++; } function appendOldChild(node) { if (!removedNode) { removedNode = node; removedNodes.add(node); } if (util_1.isText(node)) { var oldFormatting = getFormattingAncestors(oldNode, oldRootNode); formattingMap.set(node, oldFormatting); } oldOutputNode.appendChild(node); oldOutputNode = node; oldOutputDepth++; } function appendNewChild(node) { if (!addedNode) { addedNode = node; addedNodes.add(node); } if (util_1.isText(node)) { var newFormatting = getFormattingAncestors(newNode, newRootNode); formattingMap.set(node, newFormatting); } newOutputNode.appendChild(node); newOutputNode = node; newOutputDepth++; } function nextDiff(step) { var length = diffItem[1].length; diffOffset += step; if (diffOffset === length) { diffItem = diffArray[diffIndex++]; diffOffset = 0; } else { /* istanbul ignore if */ if (diffOffset > length) { return util_1.never(); } } } function nextOld(step) { var _a; var length = getLength(oldNode); oldOffset += step; if (oldOffset === length) { ; (_a = oldIterator.next(), oldDone = _a.done, oldNode = _a.value); oldOffset = 0; } else { /* istanbul ignore if */ if (oldOffset > length) { return util_1.never(); } } } function nextNew(step) { var _a; var length = getLength(newNode); newOffset += step; if (newOffset === length) { ; (_a = newIterator.next(), newDone = _a.done, newNode = _a.value); newOffset = 0; } else { /* istanbul ignore if */ if (newOffset > length) { return util_1.never(); } } } // Copy all content from oldRootNode and newRootNode to rootOutputNode, // while deduplicating identical content. // Difference markers and formatting are excluded at this stage. while (diffItem) { if (diffItem[0] === diff_match_patch_1.DIFF_DELETE) { /* istanbul ignore if */ if (oldDone) { return util_1.never(); } prepareOldOutput(); var length_2 = Math.min(diffItem[1].length - diffOffset, getLength(oldNode) - oldOffset); var text = diffItem[1].substring(diffOffset, diffOffset + length_2); appendOldChild(util_1.isText(oldNode) ? document.createTextNode(text) : oldNode.cloneNode(false)); nextDiff(length_2); nextOld(length_2); } else if (diffItem[0] === diff_match_patch_1.DIFF_INSERT) { /* istanbul ignore if */ if (newDone) { return util_1.never(); } prepareNewOutput(); var length_3 = Math.min(diffItem[1].length - diffOffset, getLength(newNode) - newOffset); var text = diffItem[1].substring(diffOffset, diffOffset + length_3); appendNewChild(util_1.isText(newNode) ? document.createTextNode(text) : newNode.cloneNode(false)); nextDiff(length_3); nextNew(length_3); } else { /* istanbul ignore if */ if (oldDone || newDone) { return util_1.never(); } prepareOldOutput(); prepareNewOutput(); var length_4 = Math.min(diffItem[1].length - diffOffset, getLength(oldNode) - oldOffset, getLength(newNode) - newOffset); var text = diffItem[1].substring(diffOffset, diffOffset + length_4); if (oldOutputNode === newOutputNode && ((util_1.isText(oldNode) && util_1.isText(newNode)) || (nodeNameOverride(oldNode.nodeName) === nodeNameOverride(newNode.nodeName) && !skipChildren(oldNode) && !skipChildren(newNode)) || util_1.areNodesEqual(oldNode, newNode))) { appendCommonChild(util_1.isText(newNode) ? document.createTextNode(text) : newNode.cloneNode(false)); } else { appendOldChild(util_1.isText(oldNode) ? document.createTextNode(text) : oldNode.cloneNode(false)); appendNewChild(util_1.isText(newNode) ? document.createTextNode(text) : newNode.cloneNode(false)); } nextDiff(length_4); nextOld(length_4); nextNew(length_4); } } // Move deletes before inserts. removedNodes.forEach(function (node) { var parentNode = node.parentNode; var previousSibling = node.previousSibling; while (previousSibling && addedNodes.has(previousSibling)) { parentNode.insertBefore(node, previousSibling); previousSibling = node.previousSibling; } }); // Ensure a user friendly result for tables. equalTables.forEach(function (equalTable) { var newTable = equalTable.newTable, oldTable = equalTable.oldTable, outputTable = equalTable.outputTable; // Handle tables which can't be diffed nicely. if (!util_1.isTableValid(oldTable, true) || !util_1.isTableValid(newTable, true) || !util_1.isTableValid(outputTable, false)) { // Remove all values which were previously recorded for outputTable. new domIterator_1.DomIterator(outputTable).forEach(function (node) { addedNodes.delete(node); removedNodes.delete(node); modifiedNodes.delete(node); formattingMap.delete(node); }); // Display both the old and new table. var parentNode = outputTable.parentNode; var oldTableClone = oldTable.cloneNode(true); var newTableClone = newTable.cloneNode(true); parentNode.insertBefore(oldTableClone, outputTable); parentNode.insertBefore(newTableClone, outputTable); parentNode.removeChild(outputTable); removedNodes.add(oldTableClone); addedNodes.add(newTableClone); return; } // Figure out which columns have been added or removed // based on the first row appearing in both tables. // // - 1: column added // - 0: column equal // - -1: column removed var columns = []; new domIterator_1.DomIterator(outputTable, trIteratorOptions).some(function (row) { var diffedRows = equalRows.get(row); if (!diffedRows) { return false; } var oldRow = diffedRows.oldRow, newRow = diffedRows.newRow; var oldColumnCount = oldRow.childNodes.length; var newColumnCount = newRow.childNodes.length; var maxColumnCount = Math.max(oldColumnCount, newColumnCount); var minColumnCount = Math.min(oldColumnCount, newColumnCount); if (row.childNodes.length === maxColumnCount) { // The generic diff algorithm worked properly in this case, // so we can rely on its results. var cells = row.childNodes; for (var i = 0, l = cells.length; i < l; ++i) { columns.push(getColumnValue(cells[i])); } } else { // Fallback to a simple but correct algorithm. var i = 0; var columnValue = 0; while (i < minColumnCount) { columns[i++] = columnValue; } columnValue = oldColumnCount < newColumnCount ? 1 : -1; while (i < maxColumnCount) { columns[i++] = columnValue; } } return true; }); var columnCount = columns.length; /* istanbul ignore if */ if (columnCount === 0) { return util_1.never(); } // Fix up the rows which do not align with `columns`. new domIterator_1.DomIterator(outputTable, trIteratorOptions).forEach(function (row) { var cells = row.childNodes; if (addedNodes.has(row) || addedNodes.has(row.parentNode)) { if (cells.length < columnCount) { for (var i = 0; i < columnCount; ++i) { if (columns[i] === -1) { var td = document.createElement('TD'); row.insertBefore(td, cells[i]); removedNodes.add(td); } } } } else if (removedNodes.has(row) || removedNodes.has(row.parentNode)) { if (cells.length < columnCount) { for (var i = 0; i < columnCount; ++i) { if (columns[i] === 1) { var td = document.createElement('TD'); row.insertBefore(td, cells[i]); } } } } else { // Check, if the columns in this row are aligned with those in the reference row. var isAligned = true; for (var i = 0, l = cells.length; i < l; ++i) { if (getColumnValue(cells[i]) !== columns[i]) { isAligned = false; break; } } if (!isAligned) { // Remove all values which were previously recorded for row's content. var iterator = new domIterator_1.DomIterator(row); iterator.next(); // Skip the row itself. iterator.forEach(function (node) { addedNodes.delete(node); removedNodes.delete(node); modifiedNodes.delete(node); formattingMap.delete(node); }); // Remove the row's content. while (row.firstChild) { row.removeChild(row.firstChild); } // Diff the individual cells. var _a = equalRows.get(row), newRow = _a.newRow, oldRow = _a.oldRow; var newCells = newRow.childNodes; var oldCells = oldRow.childNodes; var oldIndex = 0; var newIndex = 0; for (var i = 0; i < columnCount; ++i) { if (columns[i] === 1) { var newCellClone = newCells[newIndex++].cloneNode(true); row.appendChild(newCellClone); addedNodes.add(newCellClone); } else if (columns[i] === -1) { var oldCellClone = oldCells[oldIndex++].cloneNode(true); row.appendChild(oldCellClone); removedNodes.add(oldCellClone); } else { row.appendChild(visualDomDiff(oldCells[oldIndex++], newCells[newIndex++], options)); } } } } }); return; }); // Mark up the content which has been removed. removedNodes.forEach(function (node) { util_1.markUpNode(node, 'DEL', removedClass); }); // Mark up the content which has been added. addedNodes.forEach(function (node) { util_1.markUpNode(node, 'INS', addedClass); }); // Mark up the content which has been modified. if (!config.skipModified) { modifiedNodes.forEach(function (modifiedNode) { util_1.markUpNode(modifiedNode, 'INS', modifiedClass); }); } // Add formatting. formattingMap.forEach(function (formattingNodes, textNode) { formattingNodes.forEach(function (formattingNode) { var parentNode = textNode.parentNode; var previousSibling = textNode.previousSibling; if (previousSibling && util_1.areNodesEqual(previousSibling, formattingNode)) { previousSibling.appendChild(textNode); } else { var clonedFormattingNode = formattingNode.cloneNode(false); parentNode.insertBefore(clonedFormattingNode, textNode); clonedFormattingNode.appendChild(textNode); } }); }); return rootOutputNode; } exports.visualDomDiff = visualDomDiff;