Catrope has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/82790


Change subject: Main newFromDocumentInsertion commit
......................................................................

Main newFromDocumentInsertion commit

TODO write commit summary

Change-Id: I4aa980780114b391924f04df588e81c990c32983
---
M VisualEditor.hooks.php
A modules/ve-mw/test/dm/ve.dm.Transaction.test.js
M modules/ve-mw/test/dm/ve.dm.mwExample.js
M modules/ve-mw/test/index.php
M modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
M modules/ve-mw/ui/dialogs/ve.ui.MWReferenceEditDialog.js
M modules/ve-mw/ui/dialogs/ve.ui.MWReferenceInsertDialog.js
M modules/ve/dm/ve.dm.Document.js
M modules/ve/dm/ve.dm.InternalList.js
M modules/ve/dm/ve.dm.Transaction.js
M modules/ve/test/dm/ve.dm.Transaction.test.js
11 files changed, 541 insertions(+), 65 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/90/82790/1

diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index 7991879..e46ebf4 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -196,6 +196,7 @@
                                've/test/dm/ve.dm.InternalList.test.js',
                                've-mw/test/dm/ve.dm.InternalList.test.js',
                                've/test/dm/ve.dm.Transaction.test.js',
+                               've-mw/test/dm/ve.dm.Transaction.test.js',
                                've/test/dm/ve.dm.TransactionProcessor.test.js',
                                've/test/dm/ve.dm.Surface.test.js',
                                've/test/dm/ve.dm.SurfaceFragment.test.js',
diff --git a/modules/ve-mw/test/dm/ve.dm.Transaction.test.js 
b/modules/ve-mw/test/dm/ve.dm.Transaction.test.js
new file mode 100644
index 0000000..2871949
--- /dev/null
+++ b/modules/ve-mw/test/dm/ve.dm.Transaction.test.js
@@ -0,0 +1,150 @@
+/*!
+ * VisualEditor DataModel Transaction tests.
+ *
+ * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+QUnit.module( 've.dm.Transaction' );
+
+// FIXME duplicates test runner; should be using a data provider
+QUnit.test( 'newFromDocumentInsertion with references', function ( assert ) {
+       var i, j, doc2, tx, actualStoreItems, expectedStoreItems,
+               doc = ve.dm.example.createExampleDocument( 'internalData' ),
+               complexDoc = ve.dm.mwExample.createExampleDocument( 
'complexInternalData' ),
+               comment = { 'type': 'alienMeta', 'attributes': { 'domElements': 
$( '<!-- hello -->' ).get() } },
+               withReference = [
+                       { 'type': 'paragraph' },
+                       'B', 'a', 'r',
+                       { 'type': 'mwReference', 'attributes': {
+                               'mw': {},
+                               'about': '#mwt4',
+                               'listIndex': 0,
+                               'listGroup': 'mwReference/',
+                               'listKey': null,
+                               'refGroup': '',
+                               'contentsUsed': true
+                       } },
+                       { 'type': '/mwReference' },
+                       { 'type': '/paragraph' },
+                       { 'type': 'internalList' },
+                       { 'type': 'internalItem' },
+                       { 'type': 'paragraph', 'internal': { 'generated': 
'wrapper' } },
+                       'B',
+                       'a',
+                       'z',
+                       { 'type': '/paragraph' },
+                       { 'type': '/internalItem' },
+                       { 'type': '/internalList' }
+               ],
+               cases = [
+                       {
+                               'msg': 'metadata insertion',
+                               'doc': complexDoc,
+                               'range': new ve.Range( 0, 7 ),
+                               'modify': function ( newDoc ) {
+                                       newDoc.commit( 
ve.dm.Transaction.newFromMetadataInsertion(
+                                               newDoc, 5, 0, [ comment ]
+                                       ) );
+                               },
+                               'expectedOps': [
+                                       {
+                                               'type': 'replace',
+                                               'remove': complexDoc.getData( 
new ve.Range( 0, 7 ) ),
+                                               'insert': complexDoc.getData( 
new ve.Range( 0, 7 ) ),
+                                               'retainMetadata': 0,
+                                               'removeMetadata': 
complexDoc.getMetadata( new ve.Range( 0, 8 ) ),
+                                               'insertMetadata': 
complexDoc.getMetadata( new ve.Range( 0, 5 ) )
+                                                       .concat( [ [ comment ] 
] )
+                                                       .concat( 
complexDoc.getMetadata( new ve.Range( 6, 8 ) ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': complexDoc.getData( 
new ve.Range( 8, 32 ) ),
+                                               'insert': complexDoc.getData( 
new ve.Range( 8, 32 ) ),
+                                               'retainMetadata': 5,
+                                               'removeMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 33 ) ),
+                                               'insertMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 33 ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 }
+                               ]
+                       },
+                       {
+                               'msg': 'metadata removal',
+                               'doc': complexDoc,
+                               'range': new ve.Range( 24, 31 ),
+                               'modify': function ( newDoc ) {
+                                       newDoc.commit( 
ve.dm.Transaction.newFromMetadataRemoval(
+                                               newDoc, 6, new ve.Range( 0, 1 )
+                                       ) );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 8 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': complexDoc.getData( 
new ve.Range( 8, 32 ) ),
+                                               'insert': complexDoc.getData( 
new ve.Range( 8, 32 ) ),
+                                               'retainMetadata': 5,
+                                               'removeMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 33 ) ),
+                                               'insertMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 30 ) )
+                                                       .concat( [ [] ] )
+                                                       .concat( 
complexDoc.getMetadata( new ve.Range( 31, 33 ) ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 }
+                               ]
+                       },
+                       {
+                               'msg': 'inserting a brand new document; 
internal lists are merged and items renumbered',
+                               'doc': complexDoc,
+                               'range': new ve.Range( 7, 7 ),
+                               'newDocData': withReference,
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 7 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': [],
+                                               'insert': withReference.slice( 
0, 4 )
+                                                       // Renumber listIndex 
from 0 to 2
+                                                       .concat( [ 
ve.extendObject( true, {}, withReference[4],
+                                                               { 'attributes': 
{ 'listIndex': 2 } } ) ] )
+                                                       .concat( 
withReference.slice( 5, 7 ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': complexDoc.getData( 
new ve.Range( 8, 32 ) ),
+                                               'insert': complexDoc.getData( 
new ve.Range( 8, 32 ) )
+                                                       .concat( 
withReference.slice( 8, 15 ) ),
+                                               'retainMetadata': 5,
+                                               'removeMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 33 ) ),
+                                               'insertMetadata': 
complexDoc.getMetadata( new ve.Range( 13, 32 ) )
+                                                       .concat( new Array( 7 ) 
)
+                                                       .concat( 
complexDoc.getMetadata( new ve.Range( 32, 33 ) ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 }
+                               ]
+                       }
+               ];
+               QUnit.expect( 2 * cases.length );
+               for ( i = 0; i < cases.length; i++ ) {
+                       doc = ve.copyObject( cases[i].doc );
+                       if ( cases[i].newDocData ) {
+                               doc2 = new ve.dm.Document( cases[i].newDocData 
);
+                       } else {
+                               doc2 = doc.getDocumentSlice( cases[i].range );
+                               cases[i].modify( doc2 );
+                       }
+                       tx = ve.dm.Transaction.newFromDocumentInsertion( doc, 
cases[i].range, doc2 );
+                       assert.deepEqualWithDomElements( tx.getOperations(), 
cases[i].expectedOps, cases[i].msg + ': transaction' );
+
+                       actualStoreItems = [];
+                       expectedStoreItems = cases[i].expectedStoreItems || [];
+                       for ( j = 0; j < expectedStoreItems.length; j++ ) {
+                               actualStoreItems[j] = doc.store.value( 
doc.store.indexOfHash(
+                                       ve.getHash( expectedStoreItems[j] )
+                               ) );
+                       }
+                       assert.deepEqual( actualStoreItems, expectedStoreItems, 
cases[i].msg + ': store items' );
+               }
+} );
\ No newline at end of file
diff --git a/modules/ve-mw/test/dm/ve.dm.mwExample.js 
b/modules/ve-mw/test/dm/ve.dm.mwExample.js
index d343293..ad66cab 100644
--- a/modules/ve-mw/test/dm/ve.dm.mwExample.js
+++ b/modules/ve-mw/test/dm/ve.dm.mwExample.js
@@ -534,6 +534,67 @@
        { 'type': '/internalList' }
 ];
 
+ve.dm.mwExample.complexInternalData = [
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- before 
-->' ).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': 'paragraph' },
+       'F', ['o', [ve.dm.example.bold]], ['o', [ve.dm.example.italic]],
+       { 'type': 'mwReference', 'attributes': {
+               'mw': {},
+               'about': '#mwt1',
+               'listIndex': 0,
+               'listGroup': 'mwReference/',
+               'listKey': null,
+               'refGroup': '',
+               'contentsUsed': true
+       } },
+       { 'type': '/mwReference' },
+       { 'type': '/paragraph' },
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- after 
-->' ).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': 'internalList' },
+       { 'type': 'internalItem' },
+       { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
+       'R', ['e', [ve.dm.example.bold]], 'f',
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- 
reference -->' ).get() } },
+       { 'type': '/alienMeta' },
+       'e', ['r', [ve.dm.example.italic]], ['e', [ve.dm.example.italic]],
+       { 'type': 'mwReference', 'attributes': {
+               'mw': {},
+               'about': '#mwt2',
+               'listIndex': 1,
+               'listGroup': 'mwReference/',
+               'listKey': 'foo',
+               'refGroup': '',
+               'contentsUsed': true
+       } },
+       { 'type': '/mwReference' },
+       'n', 'c', 'e',
+       { 'type': '/paragraph' },
+       { 'type': '/internalItem' },
+       { 'type': 'internalItem' },
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- 
beginning -->' ).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': 'preformatted' },
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- inside 
-->' ).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': 'mwEntity', 'attributes': { 'character': '€' } },
+       { 'type': '/mwEntity' },
+       '2', '5', '0',
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- inside2 
-->' ).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': '/preformatted' },
+       { 'type': 'alienMeta', 'attributes': { 'domElements': $( '<!-- end -->' 
).get() } },
+       { 'type': '/alienMeta' },
+       { 'type': '/internalItem' },
+       { 'type': '/internalList' }
+];
+
+ve.dm.mwExample.complexInternalData.internalItems = [
+       { 'group': 'mwReference', 'key': null, 'body': 'First reference' },
+       { 'group': 'mwReference', 'key': 'foo', 'body': 'Table in ref: 
<table><tr><td>because I can</td></tr></table>' }
+];
+
 ve.dm.mwExample.domToDataCases = {
        'mw:Image': {
                'html': '<body><p>' + ve.dm.mwExample.MWInlineImageHtml + 
'</p></body>',
diff --git a/modules/ve-mw/test/index.php b/modules/ve-mw/test/index.php
index c30f562..15f42a9 100644
--- a/modules/ve-mw/test/index.php
+++ b/modules/ve-mw/test/index.php
@@ -301,6 +301,7 @@
                <script src="dm/ve.dm.MWConverter.test.js"></script>
                <script src="dm/ve.dm.InternalList.test.js"></script>
                <script src="dm/ve.dm.SurfaceFragment.test.js"></script>
+               <script src="dm/ve.dm.Transaction.test.js"></script>
                <script 
src="dm/nodes/ve.dm.MWTransclusionNode.test.js"></script>
                <script src="ce/ve.ce.ContentBranchNode.test.js"></script>
                <script src="ce/ve.ce.Document.test.js"></script>
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
index 3cb3d63..70306d9 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
@@ -61,7 +61,7 @@
 };
 
 ve.ui.MWMediaEditDialog.prototype.onOpen = function () {
-       var data, doc = this.surface.getModel().getDocument();
+       var newDoc, doc = this.surface.getModel().getDocument();
 
        // Parent method
        ve.ui.MWDialog.prototype.onOpen.call( this );
@@ -69,17 +69,17 @@
        // Get caption content
        this.captionNode = 
this.surface.getView().getFocusedNode().getModel().getCaptionNode();
        if ( this.captionNode && this.captionNode.getLength() > 0 ) {
-               data = doc.getData( this.captionNode.getRange(), true );
+               newDoc = doc.getDocumentSlice( this.captionNode );
        } else {
-               data = [
+               newDoc = [
                        { 'type': 'paragraph', 'internal': { 'generated': 
'wrapper' } },
-                       { 'type': '/paragraph' }
+                       { 'type': '/paragraph' },
+                       { 'type': 'internalList' },
+                       { 'type': '/internalList' }
                ];
        }
 
-       this.captionSurface = new ve.ui.Surface(
-               new ve.dm.ElementLinearData( doc.getStore(), data ), { '$$': 
this.frame.$$ }
-       );
+       this.captionSurface = new ve.ui.Surface( newDoc, { '$$': this.frame.$$ 
} );
        this.captionToolbar = new ve.ui.Toolbar( this.captionSurface, { '$$': 
this.frame.$$ } );
 
        this.captionToolbar.$.addClass( 've-ui-mwMediaEditDialog-toolbar' );
@@ -91,25 +91,29 @@
 };
 
 ve.ui.MWMediaEditDialog.prototype.onClose = function ( action ) {
-       var data, doc, surfaceModel = this.surface.getModel();
+       var newDoc, doc, surfaceModel = this.surface.getModel();
 
        // Parent method
        ve.ui.MWDialog.prototype.onClose.call( this );
 
        if ( action === 'apply' ) {
-               data = this.captionSurface.getModel().getDocument().getData();
+               newDoc = this.captionSurface.getModel().getDocument();
                doc = surfaceModel.getDocument();
                if ( this.captionNode ) {
                        // Replace the contents of the caption
-                       surfaceModel.getFragment( this.captionNode.getRange(), 
true ).insertContent( data );
+                       surfaceModel.change(
+                               ve.dm.Transaction.newFromDocumentInsertion( 
doc, this.captionNode, newDoc )
+                       );
                } else {
                        // Insert a new caption at the beginning of the image 
node
+                       // FIXME this doesn't handle metadata and internal 
items the way
+                       // newFromDocumentInsertion does
                        surfaceModel.getFragment()
                                .adjustRange( 1 )
                                .collapseRangeToStart()
                                .insertContent(
                                        [ { 'type': 'mwImageCaption' } ]
-                                               .concat( data )
+                                               .concat( newDoc.getData() )
                                                .concat( [ { 'type': 
'/mwImageCaption' } ] )
                                );
                }
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceEditDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceEditDialog.js
index 8dc20f3..524a82d 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceEditDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceEditDialog.js
@@ -79,7 +79,7 @@
 };
 
 ve.ui.MWReferenceEditDialog.prototype.onOpen = function () {
-       var focusedNode, data, refGroup,
+       var focusedNode, newDoc, refGroup,
                doc = this.surface.getModel().getDocument();
 
        // Parent method
@@ -88,12 +88,11 @@
        // Get reference content
        focusedNode = this.surface.getView().getFocusedNode();
        this.internalItem = focusedNode.getModel().getInternalItem();
-       data = doc.getData( this.internalItem.getRange(), true );
+       newDoc = doc.getDocumentSlice( this.internalItem );
        refGroup = focusedNode.getModel().getAttribute( 'refGroup' );
 
        // Properties
-       this.referenceSurface = new ve.ui.Surface(
-               new ve.dm.ElementLinearData( doc.getStore(), data ), { '$$': 
this.frame.$$ }
+       this.referenceSurface = new ve.ui.Surface( newDoc, { '$$': 
this.frame.$$ }
        );
        this.referenceToolbar = new ve.ui.Toolbar( this.referenceSurface, { 
'$$': this.frame.$$ } );
 
@@ -108,7 +107,7 @@
 };
 
 ve.ui.MWReferenceEditDialog.prototype.onClose = function ( action ) {
-       var i, len, txs, data, doc, group, listGroup, listKey, refGroup, 
newItem, refNode,
+       var i, len, txs, newDoc, doc, group, listGroup, listKey, refGroup, 
refNode,
                oldListGroup, refNodes, internalList, attr,
                surfaceModel = this.surface.getModel();
 
@@ -117,7 +116,7 @@
 
        // Save changes
        if ( action === 'apply' ) {
-               data = this.referenceSurface.getModel().getDocument().getData();
+               newDoc = this.referenceSurface.getModel().getDocument();
                doc = surfaceModel.getDocument();
                refGroup = this.groupInput.getValue();
                listGroup = 'mwReference/' + refGroup;
@@ -154,14 +153,10 @@
                                refNodes[i].addToInternalList();
                        }
                }
-               // Process the internal node create/edit transaction
-               if ( !newItem ) {
-                       surfaceModel.change(
-                               ve.dm.Transaction.newFromNodeReplacement( doc, 
this.internalItem, data )
-                       );
-               } else {
-                       surfaceModel.change( newItem.transaction );
-               }
+               // Process the internal node transaction
+               surfaceModel.change(
+                       ve.dm.Transaction.newFromDocumentInsertion( doc, 
this.internalItem, newDoc )
+               );
        }
 
        // Cleanup
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceInsertDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceInsertDialog.js
index 3ec27c8..4c08d70 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceInsertDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceInsertDialog.js
@@ -76,6 +76,8 @@
                                'refGroup': '',
                                'listGroup': 'mwReference/'
                        };
+                       // FIXME this doesn't handle metadata and internal 
items the way
+                       // newFromDocumentInsertion does
                        item = internalList.getItemInsertion(
                                attr.listGroup,
                                attr.listKey,
diff --git a/modules/ve/dm/ve.dm.Document.js b/modules/ve/dm/ve.dm.Document.js
index e8adbe2..26941f3 100644
--- a/modules/ve/dm/ve.dm.Document.js
+++ b/modules/ve/dm/ve.dm.Document.js
@@ -312,7 +312,7 @@
  * @throws {Error} rangeOrNode must be a ve.Range or a ve.dm.Node
  */
 ve.dm.Document.prototype.getDocumentSlice = function ( rangeOrNode ) {
-       var data, range,
+       var data, range, newDoc,
                store = this.store.clone(),
                listRange = this.internalList.getListNode().getOuterRange();
        if ( rangeOrNode instanceof ve.dm.Node ) {
@@ -327,10 +327,15 @@
                // The range does not include the entire internal list, so add 
it
                data = data.concat( this.getFullData( listRange ) );
        }
-       return new this.constructor(
+       newDoc = new this.constructor(
                new ve.dm.ElementLinearData( store, data ),
                undefined, this.internalList
        );
+       // Record the length of the internal list at the time the slice was 
created so we can
+       // reconcile additions properly
+       newDoc.origDoc = this;
+       newDoc.origInternalListLength = this.internalList.getItemNodeCount();
+       return newDoc;
 };
 
 /**
diff --git a/modules/ve/dm/ve.dm.InternalList.js 
b/modules/ve/dm/ve.dm.InternalList.js
index 9af4406..a0c74b5 100644
--- a/modules/ve/dm/ve.dm.InternalList.js
+++ b/modules/ve/dm/ve.dm.InternalList.js
@@ -409,3 +409,56 @@
 ve.dm.InternalList.prototype.clone = function ( doc ) {
        return new this.constructor( doc || this.getDocument() );
 };
+
+/**
+ * Merge another internal list into this one.
+ *
+ * This function updates the state of this list, and returns a mapping from 
indexes in list to
+ * indexes in this, as well as a set of ranges that should be copied from 
list's linear model
+ * into this list's linear model by the caller.
+ *
+ * @param {ve.dm.InternalList} list Internal list to merge into this list
+ * @param {number} commonLength The number of elements, counted from the 
beginning, that the lists have in common
+ * @returns {Object} 'mapping' is an object mapping indexes in list to indexes 
in this; newItemRanges is an array
+ *  of ranges of internal nodes in list's document that should be copied into 
our document
+ */
+ve.dm.InternalList.prototype.merge = function ( list, commonLength ) {
+       var i, k, key,
+               listLen = list.getItemNodeCount(),
+               nextIndex = this.getItemNodeCount(),
+               newItemRanges = [],
+               mapping = {};
+       for ( i = 0; i < commonLength; i++ ) {
+               mapping[i] = i;
+       }
+       for ( i = commonLength; i < listLen; i++ ) {
+               // Try to find i in list.keyIndexes
+               key = undefined;
+               for ( k in list.keyIndexes ) {
+                       if ( list.keyIndexes[k] === i ) {
+                               key = k;
+                               break;
+                       }
+               }
+
+               if ( this.keyIndexes[key] !== undefined ) {
+                       // We already have this key in this internal list. 
Ignore the duplicate that the other
+                       // list is trying to merge in.
+                       // NOTE: This case cannot occur in VE currently, but 
may be possible in the future with
+                       // collaborative editing, which is why this code needs 
to be rewritten before we do
+                       // collaborative editing.
+                       mapping[i] = this.keyIndexes[key];
+               } else {
+                       mapping[i] = nextIndex;
+                       if ( key !== undefined ) {
+                               this.keyIndexes[key] = nextIndex;
+                       }
+                       nextIndex++;
+                       newItemRanges.push( list.getItemNode( i 
).getOuterRange() );
+               }
+       }
+       return {
+               'mapping': mapping,
+               'newItemRanges': newItemRanges
+       };
+};
diff --git a/modules/ve/dm/ve.dm.Transaction.js 
b/modules/ve/dm/ve.dm.Transaction.js
index 024aeb6..d221393 100644
--- a/modules/ve/dm/ve.dm.Transaction.js
+++ b/modules/ve/dm/ve.dm.Transaction.js
@@ -154,27 +154,119 @@
 };
 
 /**
- * Generate a transaction that replaces the contents of a branch node.
+ * Build a transaction that replaces the contents of a node with the contents 
of a document.
  *
- * The node whose contents are being replaced should be an unrestricted branch 
node.
+ * This is typically used to merge changes to a document slice back into the 
main document. If newDoc
+ * is a document slice of doc, it's assumed that there were no changes to 
doc's internal list since
+ * the slice, so any differences between internal items that doc and newDoc 
have in common will
+ * be resolved in newDoc's favor.
  *
- * @param {ve.dm.Document} doc Document to create transaction for
- * @param {ve.dm.Node|ve.Range} nodeOrRange Branch node or inner range of such
- * @param {Array} newData Linear model data to replace the contents of the 
node with
- * @returns {ve.dm.Transaction} Transaction that replaces the contents of the 
node
- * @throws {Error} nodeOrRange must be a ve.dm.Node or a ve.Range
+ * @param {ve.dm.Document} doc Main document
+ * @param {ve.Range|ve.dm.Node} nodeOrRange Node or range to replace
+ * @param {ve.dm.Document} newDoc Document to insert
+ * @returns {ve.dm.Transaction} Transaction that replaces the node and updates 
the internal list
  */
-ve.dm.Transaction.newFromNodeReplacement = function ( doc, nodeOrRange, 
newData ) {
-       var tx = new ve.dm.Transaction(), range = nodeOrRange;
-       if ( range instanceof ve.dm.Node ) {
-               range = range.getRange();
-       }
-       if ( !( range instanceof ve.Range ) ) {
+ve.dm.Transaction.newFromDocumentInsertion = function ( doc, nodeOrRange, 
newDoc ) {
+       var i, len, range, merge, data, metadata, listData, listMetadata, 
oldEndOffset, newEndOffset,
+               listNode = doc.internalList.getListNode(),
+               listNodeRange = listNode.getRange(),
+               newListNode = newDoc.internalList.getListNode(),
+               newListNodeRange = newListNode.getRange(),
+               newListNodeOuterRange = newListNode.getOuterRange(),
+               tx = new ve.dm.Transaction();
+       if ( nodeOrRange instanceof ve.dm.Node ) {
+               range = nodeOrRange.getRange();
+       } else if ( nodeOrRange instanceof ve.Range ) {
+               range = nodeOrRange;
+       } else {
                throw new Error( 'nodeOrRange must be a ve.dm.Node or a 
ve.Range' );
        }
-       tx.pushRetain( range.start );
-       tx.pushReplace( doc, range.start, range.end - range.start, newData );
-       tx.pushRetain( doc.data.getLength() - range.end );
+
+       // Get the data and the metadata, but skip over the internal list
+       data = new ve.dm.ElementLinearData( doc.getStore(),
+               newDoc.getData( new ve.Range( 0, newListNodeOuterRange.start ), 
true ).concat(
+                       newDoc.getData( new ve.Range( 
newListNodeOuterRange.end, newDoc.data.getLength() ), true )
+               )
+       );
+       metadata = new ve.dm.MetaLinearData( doc.getStore(),
+               newDoc.getMetadata( new ve.Range( 0, 
newListNodeOuterRange.start ), true ).concat(
+                       // Merge the metadata immediately before and 
immediately after the internal list
+                       ve.copyArray( ve.dm.MetaLinearData.static.merge( [
+                               newDoc.metadata.getData( 
newListNodeOuterRange.start ),
+                               newDoc.metadata.getData( 
newListNodeOuterRange.end )
+                       ] ) )
+               ).concat( newDoc.getMetadata(
+                       new ve.Range( newListNodeOuterRange.end, 
newDoc.data.getLength() ), true
+               ) )
+       );
+       // Merge the stores
+       merge = doc.getStore().merge( newDoc.getStore() );
+       // Remap the store indexes in the data
+       data.remapStoreIndexes( merge );
+
+       merge = doc.internalList.merge( newDoc.internalList, 
newDoc.origInternalListLength || 0 );
+       // Remap the indexes in the data
+       data.remapInteralListIndexes( merge.mapping );
+       // Get data for the new internal list
+       if ( newDoc.origDoc === doc ) {
+               // newDoc is a document slice based on doc, so all the internal 
list items present in doc
+               // when it was cloned are also in newDoc. We need to get the 
newDoc version of these items
+               // so that changes made in newDoc are reflected.
+               oldEndOffset = doc.internalList.getItemNode( 
newDoc.origInternalListLength - 1 ).getOuterRange().end;
+               newEndOffset = newDoc.internalList.getItemNode( 
newDoc.origInternalListLength - 1 ).getOuterRange().end;
+               listData = newDoc.getData( new ve.Range( 
newListNodeRange.start, newEndOffset ), true )
+                       .concat( doc.getData( new ve.Range( oldEndOffset, 
listNodeRange.end ), true ) );
+               listMetadata = newDoc.getMetadata( new ve.Range( 
newListNodeRange.start, newEndOffset ), true )
+                       .concat( doc.getMetadata( new ve.Range( oldEndOffset, 
listNodeRange.end ) , true ) );
+       } else {
+               // newDoc is brand new, so use doc's internal list as a base
+               listData = doc.getData( listNodeRange, true );
+               listMetadata = doc.getMetadata( listNodeRange, true );
+       }
+       for ( i = 0, len = merge.newItemRanges.length; i < len; i++ ) {
+               listData = listData.concat( newDoc.getData( 
merge.newItemRanges[i], true ) );
+               // We don't have to worry about merging metadata at the edges, 
because there can't be
+               // metadata between internal list items
+               listMetadata = listMetadata.concat( newDoc.getMetadata( 
merge.newItemRanges[i], true ) );
+       }
+       // Add the metadata right after the internal list
+       listMetadata = listMetadata.concat( doc.getMetadata(
+               new ve.Range( listNodeRange.end, listNodeRange.end + 1 )
+       ) );
+
+       if ( range.end <= listNodeRange.start ) {
+               // range is entirely before listNodeRange
+               // First replace the node, then the internal list
+               tx.pushRetain( range.start );
+               tx.pushReplace( doc, range.start, range.end - range.start, 
data.data, metadata.data );
+               tx.pushRetain( listNodeRange.start - range.end );
+               tx.pushReplace( doc, listNodeRange.start, listNodeRange.end - 
listNodeRange.start,
+                       listData, listMetadata
+               );
+               tx.pushRetain( doc.data.getLength() - listNodeRange.end );
+       } else if ( listNodeRange.end <= range.start ) {
+               // range is entirely after listNodeRange
+               // First replace the internal list, then the node
+               tx.pushRetain( listNodeRange.start );
+               tx.pushReplace( doc, listNodeRange.start, listNodeRange.end - 
listNodeRange.start,
+                       listData, listMetadata
+               );
+               tx.pushRetain( range.start - listNodeRange.end );
+               tx.pushReplace( doc, range.start, range.end - range.start, 
data.data, metadata.data );
+               tx.pushRetain( doc.data.getLength() - range.end );
+       } else if ( range.start >= listNodeRange.start && range.end <= 
listNodeRange.end ) {
+               // range is entirely within listNodeRange
+               // Merge data into listData, then only replace the internal list
+               ve.batchSplice( listData, range.start - listNodeRange.start,
+                       range.end - range.start, data.data );
+               ve.batchSplice( listMetadata, range.start - listNodeRange.start,
+                       range.end - range.start + 1, metadata.data );
+               tx.pushRetain( listNodeRange.start );
+               tx.pushReplace( doc, listNodeRange.start, listNodeRange.end - 
listNodeRange.start,
+                       listData, listMetadata
+               );
+               tx.pushRetain( doc.data.getLength() - listNodeRange.end );
+       }
        return tx;
 };
 
diff --git a/modules/ve/test/dm/ve.dm.Transaction.test.js 
b/modules/ve/test/dm/ve.dm.Transaction.test.js
index 4a5c31a..ba4050a 100644
--- a/modules/ve/test/dm/ve.dm.Transaction.test.js
+++ b/modules/ve/test/dm/ve.dm.Transaction.test.js
@@ -658,38 +658,150 @@
        runConstructorTests( assert, ve.dm.Transaction.newFromRemoval, cases );
 } );
 
-QUnit.test( 'newFromNodeReplacement', function ( assert ) {
-       var doc = ve.dm.example.createExampleDocument( 'internalData' ),
-               paragraph = [ { 'type': 'paragraph' }, 'H', 'e', 'l', 'l', 'o', 
{ 'type': '/paragraph' } ],
-               secondNode = doc.internalList.getItemNode( 1 ),
-               cases = {
-                       'replacing first internal node with paragraph': {
-                               'args': [doc, new ve.Range( 7, 12 ), paragraph],
-                               'ops': [
-                                       { 'type': 'retain', 'length': 7 },
+QUnit.test( 'newFromDocumentInsertion', function ( assert ) {
+       var i, j, doc2, tx, actualStoreItems, expectedStoreItems,
+               doc = ve.dm.example.createExampleDocument( 'internalData' ),
+               nextIndex = doc.store.valueStore.length,
+               whee = [ { 'type': 'paragraph' }, 'W', 'h', 'e', 'e', { 'type': 
'/paragraph' } ],
+               wheeItem = [ { 'type': 'internalItem' } ].concat( whee 
).concat( [ { 'type': '/internalItem' } ] ),
+               cases = [
+                       {
+                               'msg': 'simple insertion',
+                               'doc': 'internalData',
+                               'range': new ve.Range( 7, 12 ),
+                               'modify': function ( newDoc ) {
+                                       // Change "Bar" to "Bazaar"
+                                       newDoc.commit( 
ve.dm.Transaction.newFromInsertion(
+                                               newDoc, 3, [ 'z', 'a', 'a' ]
+                                       ) );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 6 },
                                        {
                                                'type': 'replace',
-                                               'remove': doc.data.slice( 7, 12 
),
-                                               'insert': paragraph
+                                               'remove': doc.getData( new 
ve.Range( 6, 20 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 6, 10 ) )
+                                                       .concat( [ 'z', 'a', 
'a' ] )
+                                                       .concat( doc.getData( 
new ve.Range( 10, 20 ) ) )
                                        },
-                                       { 'type': 'retain', 'length': 15 }
+                                       { 'type': 'retain', 'length': 7 }
                                ]
                        },
-                       'replacing second internal node with two paragraphs': {
-                               'args': [doc, secondNode, paragraph.concat( 
paragraph )],
-                               'ops': [
-                                       { 'type': 'retain', 'length': 14 },
+                       {
+                               'msg': 'simple annotation',
+                               'doc': 'internalData',
+                               'range': doc.getInternalList().getItemNode( 1 ),
+                               'modify': function ( newDoc ) {
+                                       // Bold the first two characters
+                                       newDoc.commit( 
ve.dm.Transaction.newFromAnnotation(
+                                               newDoc, new ve.Range( 1, 3 ), 
'set', ve.dm.example.bold
+                                       ) );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 6 },
                                        {
                                                'type': 'replace',
-                                               'remove': 
doc.data.getDataSlice( secondNode.getRange() ),
-                                               'insert': paragraph.concat( 
paragraph )
+                                               'remove': doc.getData( new 
ve.Range( 6, 20 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 6, 15 ) )
+                                                       .concat( [ [ 
doc.data.getData( 15 ), [ nextIndex ] ] ] )
+                                                       .concat( [ [ 
doc.data.getData( 16 ), [ nextIndex ] ] ] )
+                                                       .concat( doc.getData( 
new ve.Range( 17, 20 ) ) )
                                        },
-                                       { 'type': 'retain', 'length': 8 }
+                                       { 'type': 'retain', 'length': 7 }
+                               ],
+                               'expectedStoreItems': [
+                                       ve.dm.example.bold
+                               ]
+                       },
+                       {
+                               'msg': 'insertion into internal list',
+                               'doc': 'internalData',
+                               'range': new ve.Range( 21, 27 ),
+                               'modify': function ( newDoc ) {
+                                       var insertion = 
newDoc.internalList.getItemInsertion( 'test', 'whee', whee );
+                                       newDoc.commit( insertion.transaction );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 6 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': doc.getData( new 
ve.Range( 6, 20 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 6, 20 ) ).concat( wheeItem )
+                                       },
+                                       { 'type': 'retain', 'length': 1 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': doc.getData( new 
ve.Range( 21, 27 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 21, 27 ) )
+                                       }
+                               ]
+                       },
+                       {
+                               'msg': 'change in internal list',
+                               'doc': 'internalData',
+                               'range': new ve.Range( 21, 27 ),
+                               'modify': function ( newDoc ) {
+                                       newDoc.commit( 
ve.dm.Transaction.newFromInsertion(
+                                               newDoc, 12, [ '!', '!', '!' ]
+                                       ) );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 6 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': doc.getData( new 
ve.Range( 6, 20 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 6, 11 ) )
+                                                       .concat( [ '!', '!', 
'!' ] )
+                                                       .concat( doc.getData( 
new ve.Range( 11, 20 ) ) )
+                                       },
+                                       { 'type': 'retain', 'length': 1 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': doc.getData( new 
ve.Range( 21, 27 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 21, 27 ) )
+                                       }
+                               ]
+                       },
+                       {
+                               'msg': 'insertion into internal list from slice 
within internal list',
+                               'doc': 'internalData',
+                               'range': new ve.Range( 7, 12 ),
+                               'modify': function ( newDoc ) {
+                                       var insertion = 
newDoc.internalList.getItemInsertion( 'test', 'whee', whee );
+                                       newDoc.commit( insertion.transaction );
+                               },
+                               'expectedOps': [
+                                       { 'type': 'retain', 'length': 6 },
+                                       {
+                                               'type': 'replace',
+                                               'remove': doc.getData( new 
ve.Range( 6, 20 ) ),
+                                               'insert': doc.getData( new 
ve.Range( 6, 20 ) ).concat( wheeItem )
+                                       },
+                                       { 'type': 'retain', 'length': 7 }
                                ]
                        }
-               };
-       QUnit.expect( ve.getObjectKeys( cases ).length );
-       runConstructorTests( assert, ve.dm.Transaction.newFromNodeReplacement, 
cases );
+               ];
+       QUnit.expect( 2 * cases.length );
+       for ( i = 0; i < cases.length; i++ ) {
+               doc = ve.dm.example.createExampleDocument( cases[i].doc );
+               if ( cases[i].newDocData ) {
+                       doc2 = new ve.dm.Document( cases[i].newDocData );
+               } else {
+                       doc2 = doc.getDocumentSlice( cases[i].range );
+                       cases[i].modify( doc2 );
+               }
+               tx = ve.dm.Transaction.newFromDocumentInsertion( doc, 
cases[i].range, doc2 );
+               assert.deepEqualWithDomElements( tx.getOperations(), 
cases[i].expectedOps, cases[i].msg + ': transaction' );
+
+               actualStoreItems = [];
+               expectedStoreItems = cases[i].expectedStoreItems || [];
+               for ( j = 0; j < expectedStoreItems.length; j++ ) {
+                       actualStoreItems[j] = doc.store.value( 
doc.store.indexOfHash(
+                               ve.getHash( expectedStoreItems[j] )
+                       ) );
+               }
+               assert.deepEqual( actualStoreItems, expectedStoreItems, 
cases[i].msg + ': store items' );
+       }
 } );
 QUnit.test( 'newFromAttributeChanges', function ( assert ) {
        var doc = ve.dm.example.createExampleDocument(),

-- 
To view, visit https://gerrit.wikimedia.org/r/82790
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I4aa980780114b391924f04df588e81c990c32983
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Catrope <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to