r103583 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103582‎ | r103583 | r103584 >
Date:10:17, 18 November 2011
Author:catrope
Status:deferred
Tags:
Comment:
Make es.TransactionProcessor.remove() handle deep merges correctly, and add test cases. The code is still a bit rough and ugly and needs a bit more work, but I'll clean that up later; at least it works now.
Modified paths:
  • /trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js (modified) (history)
  • /trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js (modified) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js
@@ -1,6 +1,6 @@
22 module( 'es' );
33
4 -test( 'es.TransactionProcessor', 18, function() {
 4+test( 'es.TransactionProcessor', 29, function() {
55 var documentModel = es.DocumentModel.newFromPlainObject( esTest.obj );
66
77 // FIXME: These tests shouldn't use prepareFoo() because those functions
@@ -243,4 +243,109 @@
244244 'table',
245245 'rollback keeps model tree up to date with paragraph split (table follows the paragraph)'
246246 );
 247+
 248+ var listItemMerge = documentModel.prepareRemoval( new es.Range( 14, 19 ) );
 249+
 250+ // Test 19
 251+ es.TransactionProcessor.commit( documentModel, listItemMerge );
 252+ deepEqual(
 253+ documentModel.getData( new es.Range( 12, 22 ) ),
 254+ [
 255+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
 256+ { 'type': 'paragraph' },
 257+ 'f',
 258+ { 'type': '/paragraph' },
 259+ { 'type': '/listItem' },
 260+ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
 261+ { 'type': 'paragraph' },
 262+ 'g',
 263+ { 'type': '/paragraph' },
 264+ { 'type': '/listItem' }
 265+ ],
 266+ 'removal merges two list items with paragraphs'
 267+ );
 268+
 269+ // Test 20
 270+ deepEqual( documentModel.children[1].children[0].children[0].children[1].children.length, 2,
 271+ 'removal keeps model tree up to date with list item merge (number of children)'
 272+ );
 273+
 274+ // Test 21
 275+ deepEqual(
 276+ documentModel.children[1].children[0].children[0].children[1].children[0].children[0].getContent(),
 277+ [ 'f' ],
 278+ 'removal keeps model tree up to date with list item merge (first list item)'
 279+ );
 280+
 281+ // Test 22
 282+ deepEqual(
 283+ documentModel.children[1].children[0].children[0].children[1].children[1].children[0].getContent(),
 284+ [ 'g' ],
 285+ 'removal keeps model tree up to date with list item merge (second list item)'
 286+ );
 287+
 288+ // Test 23
 289+ deepEqual(
 290+ documentModel.children[2].getContent(),
 291+ [ 'h' ],
 292+ 'rollback keeps model tree up to date with list item split (final paragraph)'
 293+ );
 294+
 295+ // Test 24
 296+ es.TransactionProcessor.rollback( documentModel, listItemMerge );
 297+ deepEqual(
 298+ documentModel.getData( new es.Range( 12, 27 ) ),
 299+ [
 300+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
 301+ { 'type': 'paragraph' },
 302+ 'e',
 303+ { 'type': '/paragraph' },
 304+ { 'type': '/listItem' },
 305+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
 306+ { 'type': 'paragraph' },
 307+ 'f',
 308+ { 'type': '/paragraph' },
 309+ { 'type': '/listItem' },
 310+ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
 311+ { 'type': 'paragraph' },
 312+ 'g',
 313+ { 'type': '/paragraph' },
 314+ { 'type': '/listItem' }
 315+ ],
 316+ 'rollback reverses list item merge (splits the list items)'
 317+ );
 318+
 319+ // Test 25
 320+ deepEqual( documentModel.children[1].children[0].children[0].children[1].children.length, 3,
 321+ 'rollback keeps model tree up to date with list item split (number of children)'
 322+ );
 323+
 324+ // Test 26
 325+ deepEqual(
 326+ documentModel.children[1].children[0].children[0].children[1].children[0].children[0].getContent(),
 327+ [ 'e' ],
 328+ 'rollback keeps model tree up to date with list item split (first list item)'
 329+ );
 330+
 331+ // Test 27
 332+ deepEqual(
 333+ documentModel.children[1].children[0].children[0].children[1].children[1].children[0].getContent(),
 334+ [ 'f' ],
 335+ 'rollback keeps model tree up to date with list item split (second list item)'
 336+ );
 337+
 338+ // Test 28
 339+ deepEqual(
 340+ documentModel.children[1].children[0].children[0].children[1].children[2].children[0].getContent(),
 341+ [ 'g' ],
 342+ 'rollback keeps model tree up to date with list item split (third list item)'
 343+ );
 344+
 345+ // Test 29
 346+ deepEqual(
 347+ documentModel.children[2].getContent(),
 348+ [ 'h' ],
 349+ 'rollback keeps model tree up to date with list item split (final paragraph)'
 350+ );
 351+
247352 } );
Index: trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
@@ -88,17 +88,23 @@
8989 }
9090 };
9191
 92+// TODO: document this. Various arguments are optional or nonoptional in different cases, that's confusing
 93+// so it needs to be documented well.
9294 es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes, parent, index ) {
9395 var newNodes = es.DocumentModel.createNodesFromData( newData ),
9496 remove = 0;
9597 if ( oldNodes ) {
 98+ // Determine parent and index if not given
9699 if ( oldNodes[0] === oldNodes[0].getRoot() ) {
 100+ // We know the values for parent and index in this case
 101+ // and don't have to compute them. Override any parent
 102+ // or index parameter passed.
97103 parent = oldNodes[0];
98104 index = 0;
99105 remove = parent.getChildren().length;
100106 } else {
101 - parent = oldNodes[0].getParent();
102 - index = parent.indexOf( oldNodes[0] );
 107+ parent = parent || oldNodes[0].getParent();
 108+ index = index || parent.indexOf( oldNodes[0] );
103109 remove = oldNodes.length;
104110 }
105111 // Try to preserve the first node
@@ -264,16 +270,16 @@
265271 if ( es.DocumentModel.containsElementData( op.data ) ) {
266272 // Figure out which nodes are covered by the removal
267273 var ranges = this.model.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) );
268 - var oldNodes = [], newData = [], firstKeptNode = true, lastElement;
 274+
 275+ // Build the list of nodes to rebuild and the data to keep
 276+ var oldNodes = [], newData = [], parent = null, index = null, firstKeptNode, lastKeptNode;
269277 for ( var i = 0; i < ranges.length; i++ ) {
270278 oldNodes.push( ranges[i].node );
271279 if ( ranges[i].range !== undefined ) {
272280 // We have to keep part of this node
273 - if ( firstKeptNode ) {
 281+ if ( firstKeptNode === undefined ) {
274282 // This is the first node we're keeping
275 - // Keep its opening as well
276 - newData.push( ranges[i].node.getElement() );
277 - firstKeptNode = false;
 283+ firstKeptNode = ranges[i].node;
278284 }
279285 // Compute the start and end offset of this node
280286 // We could do that with getOffsetFromNode() but
@@ -288,17 +294,76 @@
289295 // Append it to newData
290296 newData = newData.concat( nodeData );
291297
292 - lastElement = ranges[i].node.getElementType();
 298+ lastKeptNode = ranges[i].node;
293299 }
294300 }
295 - if ( lastElement !== undefined ) {
296 - // Keep the closing of the last element that was partially kept
297 - newData.push( { 'type': '/' + lastElement } );
 301+
 302+ // Surround newData with the right openings and closings if needed
 303+ if ( firstKeptNode !== undefined ) {
 304+ // There are a number of conceptually different cases here,
 305+ // but the algorithm for dealing with them is the same.
 306+ // 1. Removal within one node: firstKeptNode === lastKeptNode
 307+ // 2. Merge of siblings: firstKeptNode.getParent() === lastKeptNode.getParent()
 308+ // 3. Merge of arbitrary depth: firstKeptNode and lastKeptNode have a common ancestor
 309+ // Because #1 and #2 are special cases of #3 (merges with depth=0 and depth=1, respectively),
 310+ // the code below that deals with the general case (#3) and automatically covers
 311+ // #1 and #2 that way as well.
 312+
 313+ // Simultaneously traverse upwards from firstKeptNode and lastKeptNode
 314+ // to find the common ancestor. On our way up, keep the element of each
 315+ // node we visit and verify that the transaction is a valid merge (i.e. it satisfies
 316+ // the merge criteria in prepareRemoval()'s canMerge()).
 317+ // FIXME: The code is essentially the same as canMerge(), merge these algorithms
 318+ var n1 = firstKeptNode, n2 = lastKeptNode,
 319+ prevN1 = null, prevN2 = null,
 320+ openings = [], closings = [];
 321+ while ( n1 !== n2 ) {
 322+ // Verify the element types are equal
 323+ if ( n1.getElementType() !== n2.getElementType() ) {
 324+ throw 'Removal is not a valid merge: corresponding parents have different types ( ' +
 325+ n1.getElementType() + ' vs ' + n2.getElementType() + ' )';
 326+ }
 327+ // Record the opening of n1 and the closing of n2
 328+ openings.push( n1.getElement() );
 329+ closings.push( { 'type': '/' + n2.getElementType() } );
 330+ // Move up
 331+ prevN1 = n1;
 332+ prevN2 = n2;
 333+ n1 = n1.getParent();
 334+ n2 = n2.getParent();
 335+ if ( n1 === null || n2 === null ) {
 336+ // Reached a root, so no common ancestor or different depth
 337+ throw 'Removal is not a valid merge: nodes do not have a common ancestor or are not at the same depth';
 338+ }
 339+ }
 340+
 341+ // Surround newData with the openings and closings
 342+ newData = openings.reverse().concat( newData, closings );
 343+
 344+ // Rebuild oldNodes if needed
 345+ // This only happens for merges with depth > 1
 346+ if ( prevN1 !== oldNodes[0] ) {
 347+ oldNodes = [ prevN1 ];
 348+ parent = n1;
 349+ index = n1.indexOf( prevN1 ); // Pass to rebuildNodes() so it's not recomputed
 350+ if ( index === -1 ) {
 351+ throw "Tree corruption detected: node isn't in its parent's children array";
 352+ }
 353+ var foundPrevN2 = false;
 354+ for ( var j = index + 1; !foundPrevN2 && j < n1.getChildren().length; j++ ) {
 355+ oldNodes.push( n1.getChildren()[j] );
 356+ foundPrevN2 = n1.getChildren()[j] === prevN2;
 357+ }
 358+ if ( !foundPrevN2 ) {
 359+ throw "Tree corruption detected: node isn't in its parent's children array";
 360+ }
 361+ }
298362 }
 363+
299364 // Update the linear model
300365 this.model.data.splice( this.cursor, op.data.length );
301366 // Perform the rebuild. This updates the model tree
302 - this.rebuildNodes( newData, oldNodes );
 367+ this.rebuildNodes( newData, oldNodes, parent, index );
303368 } else {
304369 // We're removing content only. Take a shortcut
305370 // Get the node we are removing content from

Status & tagging log