jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/340656 )
Change subject: Generalize DiffElement logic (improves lists and tables)
......................................................................
Generalize DiffElement logic (improves lists and tables)
In particular, distinguish between structural differences
and content differences.
This vastly improves list handling, and paves the way for
structural list diffs to be described like annotation
changes, so removed/inserted highlighting is reserved for
actual content.
It also improves the appearance of table diffs.
Bug: T149537
Bug: T158518
Change-Id: I997793770b150739df32f15c2f2ac1454ee9c9c7
---
M src/ui/elements/ve.ui.DiffElement.js
M tests/ui/ve.ui.DiffElement.test.js
2 files changed, 203 insertions(+), 116 deletions(-)
Approvals:
Esanders: Looks good to me, approved
jenkins-bot: Verified
diff --git a/src/ui/elements/ve.ui.DiffElement.js
b/src/ui/elements/ve.ui.DiffElement.js
index f08f8e8..70a1126 100644
--- a/src/ui/elements/ve.ui.DiffElement.js
+++ b/src/ui/elements/ve.ui.DiffElement.js
@@ -200,15 +200,11 @@
*/
ve.ui.DiffElement.prototype.getChangedNodeElements = function ( oldNodeIndex,
move ) {
var i, ilen, j, jlen, k, klen,
- iModified, jModified, element, body,
+ newIndex, oldIndex, element, body,
newNodeIndex = this.oldToNew[ oldNodeIndex ].node,
nodeRange = this.newDocChildren[ newNodeIndex ].getOuterRange(),
documentSlice = this.newDoc.cloneFromRange( nodeRange ),
nodeData = documentSlice.data.data,
- alreadyProcessed = {
- remove: {},
- insert: {}
- },
diff = this.oldToNew[ oldNodeIndex ].diff,
treeDiff = diff.treeDiff,
diffInfo = diff.diffInfo,
@@ -216,7 +212,9 @@
newTree = diff.newTree,
oldNodes = oldTree.orderedNodes,
newNodes = newTree.orderedNodes,
- correspondingNodes = this.oldToNew[ oldNodeIndex
].correspondingNodes;
+ correspondingNodes = this.oldToNew[ oldNodeIndex
].correspondingNodes,
+ structuralRemoves = [],
+ highestRemovedAncestors = {};
/**
* Splice in the removed data for the subtree rooted at this node, from
the old
@@ -225,58 +223,108 @@
* @param {number} nodeIndex The index of this node in the subtree
rooted at
* this document child
*/
- function highlightRemovedSubTree( nodeIndex ) {
- var i, ilen, subTreeRootNode, subTreeRootNodeData, siblingNodes,
- newPreviousNodeIndex, oldPreviousNodeIndex,
insertIndex, descendants;
+ function highlightRemovedNode( nodeIndex ) {
+ var i, ilen, node, removeData, siblingNodes,
+ newPreviousNodeIndex, oldPreviousNodeIndex, insertIndex,
+ highestRemovedAncestor;
- // Get outer data for this node from the old doc and add remove
class
- subTreeRootNode = oldNodes[ nodeIndex ];
- subTreeRootNodeData = this.oldDoc.getData(
subTreeRootNode.node.getOuterRange() );
- subTreeRootNodeData[ 0 ] = this.addAttributesToNode(
subTreeRootNodeData[ 0 ], this.oldDoc, { 'data-diff-action': 'remove' } );
-
- // If this node is a child of the document node, then it won't
have a "previous
- // node" (see below), in which case, insert it just before its
corresponding
- // node in the new document.
- if ( subTreeRootNode.index === 0 ) {
- insertIndex = newNodes[ correspondingNodes.oldToNew[ 0
] ]
- .node.getOuterRange().from - nodeRange.from;
- } else {
-
- // Find the node that corresponds to the "previous
node" of this node. The
- // "previous node" is either:
- // - the rightmost left sibling that corresponds to a
node in the new document
- // - or if there isn't one, then this node's parent
(which must correspond to
- // a node in the new document, or this node would have
been marked already
- // processed)
- siblingNodes = subTreeRootNode.parent.children;
- for ( i = 0, ilen = siblingNodes.length; i < ilen; i++
) {
- if ( siblingNodes[ i ].index === nodeIndex ) {
- break;
- } else {
- oldPreviousNodeIndex = siblingNodes[ i
].index;
- newPreviousNodeIndex =
correspondingNodes.oldToNew[ oldPreviousNodeIndex ] || newPreviousNodeIndex;
- }
- }
-
- // If previous node was found among siblings, insert
the removed subtree just
- // after its corresponding node in the new document.
Otherwise insert the
- // removed subtree just inside its parent node's
corresponding node.
- if ( newPreviousNodeIndex ) {
- insertIndex = newNodes[ newPreviousNodeIndex
].node.getOuterRange().to - nodeRange.from;
+ function findRemovedAncestor( node ) {
+ if ( !node.parent || structuralRemoves.indexOf(
node.parent.index ) === -1 ) {
+ return node.index;
} else {
- newPreviousNodeIndex =
correspondingNodes.oldToNew[ subTreeRootNode.parent.index ];
- insertIndex = newNodes[ newPreviousNodeIndex
].node.getOuterRange().from - nodeRange.from;
+ return findRemovedAncestor( node.parent );
}
-
}
- ve.batchSplice( nodeData, insertIndex, 0, subTreeRootNodeData );
+ function getRemoveData( node, index ) {
+ var removeData, tempData;
- // Mark all children as already processed
- // In the future, may also annotate all descendants
- descendants = oldTree.getNodeDescendants( subTreeRootNode );
- for ( i = 0, ilen = descendants.length; i < ilen; i++ ) {
- alreadyProcessed.remove[ descendants[ i ].index ] =
true;
+ removeData = this.oldDoc.getData(
node.node.getOuterRange() );
+ removeData[ 0 ] = this.addAttributesToNode( removeData[
0 ], this.oldDoc, {
+ 'data-diff-action': 'remove'
+ } );
+
+ while ( node && node.index !== index ) {
+ node = node.parent;
+ tempData = this.oldDoc.getData(
node.node.getOuterRange() );
+ removeData.unshift( tempData[ 0 ] );
+ removeData.push( tempData[ tempData.length - 1
] );
+ removeData[ 0 ] = this.addAttributesToNode(
removeData[ 0 ], this.oldDoc, {
+ 'data-diff-action': 'structural-remove'
+ } );
+ }
+
+ return removeData;
+ }
+
+ node = oldNodes[ nodeIndex ];
+
+ if ( !( node.node instanceof ve.dm.ContentBranchNode ) ) {
+
+ // Record that the node has been removed, but don't
display it, for now
+ // TODO: describe the change for the attribute diff
+ structuralRemoves.push( nodeIndex );
+
+ } else {
+
+ // Display the removed node, and all its ancestors, up
to the first ancestor that
+ // hasn't been removed.
+ highestRemovedAncestor = oldNodes[ findRemovedAncestor(
node ) ];
+ removeData = getRemoveData.call( this, node,
highestRemovedAncestor.index );
+
+ // Work out where to insert the removed subtree
+ if ( highestRemovedAncestor.index in
highestRemovedAncestors ) {
+
+ // The highest removed ancestor has already
been spliced into nodeData, so remove
+ // it from this subtree and splice the rest of
this subtree in
+ removeData.shift();
+ removeData.pop();
+ insertIndex = highestRemovedAncestors[
highestRemovedAncestor.index ];
+
+ } else if ( !highestRemovedAncestor.parent ) {
+
+ // If this node is a child of the document
node, then it won't have a "previous
+ // node" (see below), in which case, insert it
just before its corresponding
+ // node in the new document.
+ insertIndex = newNodes[
correspondingNodes.oldToNew[ highestRemovedAncestor.index ] ]
+ .node.getOuterRange().from -
nodeRange.from;
+
+ } else {
+
+ // Find the node that corresponds to the
"previous node" of this node. The
+ // "previous node" is either:
+ // - the rightmost left sibling that
corresponds to a node in the new document
+ // - or if there isn't one, then this node's
parent (which must correspond to
+ // a node in the new document, or this node
would have been marked already
+ // processed)
+ siblingNodes =
highestRemovedAncestor.parent.children;
+ for ( i = 0, ilen = siblingNodes.length; i <
ilen; i++ ) {
+ if ( siblingNodes[ i ].index ===
highestRemovedAncestor.index ) {
+ break;
+ } else {
+ oldPreviousNodeIndex =
siblingNodes[ i ].index;
+ newPreviousNodeIndex =
correspondingNodes.oldToNew[ oldPreviousNodeIndex ] || newPreviousNodeIndex;
+ }
+ }
+
+ // If previous node was found among siblings,
insert the removed subtree just
+ // after its corresponding node in the new
document. Otherwise insert the
+ // removed subtree just inside its parent
node's corresponding node.
+ if ( newPreviousNodeIndex ) {
+ insertIndex = newNodes[
newPreviousNodeIndex ].node.getOuterRange().to - nodeRange.from;
+ } else {
+ newPreviousNodeIndex =
correspondingNodes.oldToNew[ highestRemovedAncestor.parent.index ];
+ insertIndex = newNodes[
newPreviousNodeIndex ].node.getRange().from - nodeRange.from;
+ }
+
+ // If more content branch node descendants of
the highest removed node have
+ // also been removed, record the index where
their subtrees will need to be
+ // spliced in.
+ highestRemovedAncestors[
highestRemovedAncestor.index ] = insertIndex + 1;
+
+ }
+
+ ve.batchSplice( nodeData, insertIndex, 0, removeData );
}
}
@@ -286,24 +334,19 @@
* @param {number} nodeIndex The index of this node in the subtree
rooted at
* this document child
*/
- function highlightInsertedSubTree( nodeIndex ) {
- var i, ilen, subTreeRootNode, subTreeRootNodeRangeStart,
descendants;
+ function highlightInsertedNode( nodeIndex ) {
+ var node, nodeRangeStart;
// Find index of first data element for this node
- subTreeRootNode = newNodes[ nodeIndex ];
- subTreeRootNodeRangeStart =
subTreeRootNode.node.getOuterRange().from - nodeRange.from;
+ node = newNodes[ nodeIndex ];
+ nodeRangeStart = node.node.getOuterRange().from -
nodeRange.from;
// Add insert class
- nodeData[ subTreeRootNodeRangeStart ] =
this.addAttributesToNode(
- nodeData[ subTreeRootNodeRangeStart ], this.newDoc, {
'data-diff-action': 'insert' }
+ nodeData[ nodeRangeStart ] = this.addAttributesToNode(
+ nodeData[ nodeRangeStart ], this.newDoc, {
+ 'data-diff-action': node.node instanceof
ve.dm.ContentBranchNode ? 'insert' : 'structural-insert'
+ }
);
-
- // Mark all children as already processed
- // In the future, may also annotate all descendants
- descendants = newTree.getNodeDescendants( subTreeRootNode );
- for ( i = 0, ilen = descendants.length; i < ilen; i++ ) {
- alreadyProcessed.insert[ descendants[ i ].index ] =
true;
- }
}
/**
@@ -312,30 +355,30 @@
*
* @param {number} nodeIndex The index of this node in the subtree
rooted at
* this document child
+ * @param {Object} diffInfo Information relating to this node's change
*/
- function highlightChangedSubTree( nodeIndex ) {
- var subTreeRootNode, subTreeRootNodeRangeStart,
subTreeRootNodeData, annotatedData,
- subTreeDiffInfo = diffInfo[ k ];
+ function highlightChangedNode( nodeIndex, diffInfo ) {
+ var node, nodeRangeStart, nodeDiffData, annotatedData;
// The new node was changed.
// Get data for this node
- subTreeRootNode = newNodes[ nodeIndex ];
- subTreeRootNodeRangeStart =
subTreeRootNode.node.getOuterRange().from - nodeRange.from;
+ node = newNodes[ nodeIndex ];
+ nodeRangeStart = node.node.getOuterRange().from -
nodeRange.from;
- if ( subTreeDiffInfo.linearDiff ) {
+ if ( diffInfo.linearDiff ) {
// If there is a content change, splice it in
- subTreeRootNodeData = subTreeDiffInfo.linearDiff;
- annotatedData = this.annotateNode( subTreeRootNodeData
);
- ve.batchSplice( nodeData, subTreeRootNodeRangeStart +
1, subTreeRootNode.node.length, annotatedData );
+ nodeDiffData = diffInfo.linearDiff;
+ annotatedData = this.annotateNode( nodeDiffData );
+ ve.batchSplice( nodeData, nodeRangeStart + 1,
node.node.length, annotatedData );
} else {
// If there is no content change, just add change class
- nodeData[ subTreeRootNodeRangeStart ] =
this.addAttributesToNode(
- nodeData[ subTreeRootNodeRangeStart ],
this.newDoc, { 'data-diff-action': 'change' }
+ nodeData[ nodeRangeStart ] = this.addAttributesToNode(
+ nodeData[ nodeRangeStart ], this.newDoc, {
'data-diff-action': 'structural-change' }
);
}
- // TODO: render type and attribute changes according to
subTreeDiffInfo.typeChange
- // and subTreeDiffInfo.attributeChange
+ // TODO: render type and attribute changes according to
diffInfo.typeChange
+ // and diffInfo.attributeChange
}
// Iterate backwards over trees so that changes are made from right to
left
@@ -344,62 +387,51 @@
jlen = ilen;
for ( i = 0, j = 0; i < ilen && j < jlen; i++, j++ ) {
- iModified = newNodes.length - 1 - i;
- jModified = oldNodes.length - 1 - j;
+ newIndex = newNodes.length - 1 - i;
+ oldIndex = oldNodes.length - 1 - j;
- if ( iModified < 0 ) {
+ if ( newIndex < 0 ) {
// The rest of the nodes have been removed
- if ( !( jModified in alreadyProcessed.remove ) ) {
- highlightRemovedSubTree.call( this, jModified );
- }
+ highlightRemovedNode.call( this, oldIndex );
- } else if ( jModified < 0 ) {
+ } else if ( oldIndex < 0 ) {
// The rest of the nodes have been inserted
- if ( !( iModified in alreadyProcessed.insert ) ) {
- highlightInsertedSubTree.call( this, iModified
);
- }
+ highlightInsertedNode.call( this, newIndex );
- } else if ( correspondingNodes.newToOld[ iModified ] ===
jModified ) {
+ } else if ( correspondingNodes.newToOld[ newIndex ] ===
oldIndex ) {
// The new node was changed.
for ( k = 0, klen = treeDiff.length; k < klen; k++ ) {
- if ( treeDiff[ k ][ 0 ] === jModified &&
treeDiff[ k ][ 1 ] === iModified ) {
- if ( !( jModified in
alreadyProcessed.remove ) &&
- !( iModified in
alreadyProcessed.insert ) ) {
+ if ( treeDiff[ k ][ 0 ] === oldIndex &&
treeDiff[ k ][ 1 ] === newIndex ) {
- if ( diffInfo[ k ].replacement
) {
+ if ( diffInfo[ k ].replacement ) {
- // We are treating
these nodes as removed and inserted
-
highlightRemovedSubTree.call( this, jModified );
-
highlightInsertedSubTree.call( this, iModified );
+ // We are treating these nodes
as removed and inserted
+ highlightRemovedNode.call(
this, oldIndex );
+ highlightInsertedNode.call(
this, newIndex );
- } else {
+ } else {
- // There could be any
combination of content, attribute and type changes
-
highlightChangedSubTree.call( this, iModified, jModified );
-
- }
+ // There could be any
combination of content, attribute and type changes
+ highlightChangedNode.call(
this, newIndex, diffInfo[ k ] );
}
+
}
}
- } else if ( correspondingNodes.newToOld[ iModified ] ===
undefined ) {
+ } else if ( correspondingNodes.newToOld[ newIndex ] ===
undefined ) {
// The new node was inserted.
- if ( !( iModified in alreadyProcessed.insert ) ) {
- highlightInsertedSubTree.call( this, iModified
);
- }
+ highlightInsertedNode.call( this, newIndex );
j--;
- } else if ( correspondingNodes.newToOld[ iModified ] <
jModified ) {
+ } else if ( correspondingNodes.newToOld[ newIndex ] < oldIndex
) {
// The old node was removed.
- if ( !( jModified in alreadyProcessed.remove ) ) {
- highlightRemovedSubTree.call( this, jModified );
- }
+ highlightRemovedNode.call( this, oldIndex );
i--;
}
diff --git a/tests/ui/ve.ui.DiffElement.test.js
b/tests/ui/ve.ui.DiffElement.test.js
index b0b6ae3..73f6baf 100644
--- a/tests/ui/ve.ui.DiffElement.test.js
+++ b/tests/ui/ve.ui.DiffElement.test.js
@@ -90,7 +90,7 @@
newDoc: '<figure><img
src="boo.jpg"><figcaption>bar</figcaption></figure>',
expected:
'<div
class="ve-ui-diffElement-doc-child-change">' +
- '<figure
data-diff-action="change"><img src="boo.jpg" width="0" height="0"
alt="null"><figcaption>bar</figcaption></figure>' +
+ '<figure
data-diff-action="structural-change"><img src="boo.jpg" width="0" height="0"
alt="null"><figcaption>bar</figcaption></figure>' +
'</div>'
},
{
@@ -99,7 +99,7 @@
newDoc: '<figure class="ve-align-right"><img
src="boo.jpg"><figcaption>bar</figcaption></figure>',
expected:
'<div
class="ve-ui-diffElement-doc-child-change">' +
- '<figure class="ve-align-right"
data-diff-action="change"><img src="boo.jpg" width="0" height="0"
alt="null"><figcaption>bar</figcaption></figure>' +
+ '<figure class="ve-align-right"
data-diff-action="structural-change"><img src="boo.jpg" width="0" height="0"
alt="null"><figcaption>bar</figcaption></figure>' +
'</div>'
},
{
@@ -161,8 +161,10 @@
expected:
'<div
class="ve-ui-diffElement-doc-child-change">' +
'<table><tbody>' +
- '<tr><td>A</td><td
data-diff-action="insert">B</td></tr>' +
- '<tr><td>C</td><td
data-diff-action="insert">D</td></tr>' +
+ '<tr><td>A</td>' +
+ '<td
data-diff-action="structural-insert"><p
data-diff-action=\"insert\">B</p></td></tr>' +
+ '<tr><td>C</td>' +
+ '<td
data-diff-action="structural-insert"><p
data-diff-action=\"insert\">D</p></td></tr>' +
'</tbody></table>' +
'</div>'
},
@@ -173,8 +175,8 @@
expected:
'<div
class="ve-ui-diffElement-doc-child-change">' +
'<table><tbody>' +
- '<tr><td>A</td><td
data-diff-action="remove">B</td></tr>' +
- '<tr><td>C</td><td
data-diff-action="remove">D</td></tr>' +
+ '<tr><td>A</td><td
data-diff-action="structural-remove"><p
data-diff-action=\"remove\">B</p></td></tr>' +
+ '<tr><td>C</td><td
data-diff-action="structural-remove"><p
data-diff-action=\"remove\">D</p></td></tr>' +
'</tbody></table>' +
'</div>'
},
@@ -193,7 +195,11 @@
'<div
class="ve-ui-diffElement-doc-child-change">' +
'<table><tbody>' +
'<tr><td>A</td><td>B</td><td><del data-diff-action="remove">C</del><ins
data-diff-action="insert">X</ins></td></tr>' +
- '<tr
data-diff-action="remove"><td>D</td><td>E</td><td>F</td></tr>' +
+ '<tr
data-diff-action="structural-remove">' +
+ '<td
data-diff-action="structural-remove"><p data-diff-action=\"remove\">D</p></td>'
+
+ '<td
data-diff-action="structural-remove"><p data-diff-action=\"remove\">E</p></td>'
+
+ '<td
data-diff-action="structural-remove"><p data-diff-action=\"remove\">F</p></td>'
+
+ '</tr>' +
'<tr><td>G</td><td>H</td><td><del data-diff-action="remove">I</del><ins
data-diff-action="insert">Y</ins></td></tr>' +
'</tbody></table>' +
'</div>'
@@ -259,6 +265,55 @@
' baz' +
'</p>' +
'</div>'
+ },
+ {
+ msg: 'List item insertion',
+ oldDoc:
'<ul><li><p>foo</p></li><li><p>baz</p></li></ul>',
+ newDoc:
'<ul><li><p>foo</p></li><li><p>bar</p></li><li><p>baz</p></li></ul>',
+ expected:
+ '<div
class="ve-ui-diffElement-doc-child-change">' +
+ '<ul>' +
+ '<li><p>foo</p></li>' +
+ '<li
data-diff-action=\"structural-insert\"><p
data-diff-action=\"insert\">bar</p></li>' +
+ '<li><p>baz</p></li>' +
+ '</ul>' +
+ '</div>'
+ },
+ {
+ msg: 'List item removal',
+ oldDoc:
'<ul><li><p>foo</p></li><li><p>bar</p></li><li><p>baz</p></li></ul>',
+ newDoc:
'<ul><li><p>foo</p></li><li><p>baz</p></li></ul>',
+ expected:
+ '<div
class="ve-ui-diffElement-doc-child-change">' +
+ '<ul>' +
+ '<li><p>foo</p></li>' +
+ '<li
data-diff-action=\"structural-remove\"><p
data-diff-action=\"remove\">bar</p></li>' +
+ '<li><p>baz</p></li>' +
+ '</ul>' +
+ '</div>'
+ },
+ {
+ msg: 'List item indentation',
+ oldDoc: '<ul><li><p>foo</p></li></ul>',
+ newDoc:
'<ul><li><ul><li><p>foo</p></li></ul></li></ul>',
+ expected:
+ '<div
class="ve-ui-diffElement-doc-child-change">' +
+ '<ul
data-diff-action=\"structural-insert\">' +
+ '<li
data-diff-action=\"structural-insert\"><ul><li><p>foo</p></li></ul></li>' +
+ '</ul>' +
+ '</div>'
+ },
+ {
+ msg: 'List item deindentation',
+ oldDoc:
'<ul><li><ul><li><p>foo</p></li></ul></li></ul>',
+ newDoc: '<ul><li><p>foo</p></li></ul>',
+ expected:
+ '<div
class="ve-ui-diffElement-doc-child-change">' +
+ '<ul>' +
+ // TODO: Add remove
classes here
+ '<li><p>foo</p></li>' +
+ '</ul>' +
+ '</div>'
}
];
--
To view, visit https://gerrit.wikimedia.org/r/340656
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I997793770b150739df32f15c2f2ac1454ee9c9c7
Gerrit-PatchSet: 4
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Tchanders <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Tchanders <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits