Catrope has uploaded a new change for review.

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


Change subject: [WIP] ve.dm.Transaction: Implement newFromDocumentInsertion, 
take 2
......................................................................

[WIP] ve.dm.Transaction: Implement newFromDocumentInsertion, take 2

TODO update commit summary

This function builds a transaction that takes a document slice and
inserts it back into the document it came from, applying any changes
that were made.

This makes editing document slices simple:
    slicedDoc = doc.getDocumentSlice( captionNode );
    // Edit slicedDoc using a surface
    tx = ve.dm.Transaction.newFromDocumentInsertion( doc, captionNode, 
slicedDoc );
    surface.change( tx );

Specifically, newFromDocumentInsertion replaces the node's contents
with the document's contents (meaning any changes made to the node in
the meantime are lost). It also merges the stores internal lists
of the two documents and remaps indexes accordingly. This means editing
of references inside of references is supported.

This functionality is not specific to slices, and can also be used to
safely insert data from a paste buffer, with internal list data being
transplanted correctly.

ve.dm.MetaLinearData:
* Make merge( [ undefined, undefined, ... ] ) return undefined rather
  than [].

ve.dm.Document:
* In getDocumentSlice, store a pointer to the original dm.Document in
  the new one, and also store the length of the internal list. This
  allows us to figure out which internal list items the two documents
  have in common when we insert the modified slice back into the main
  document.
* In getMetadataReplace, optionally take the inserted metadata as a
  parameter, to allow for operations that insert both data and metadata.
  Per Ed's review, rewrite this function to return null rather than {}
  if no metadata needs to be replaced.

ve.dm.InternalList:
* Add method to merge two internal lists

ve.dm.Transaction:
* Remove newFromNodeReplacement and replace it with newFromDocumentInsertion.
* In pushReplace, optionally take the inserted metadata as a parameter.

Change-Id: I48bf5b8aef02a51947a2c3ebcfe2fa7afe90a31a
---
M modules/ve/dm/lineardata/ve.dm.MetaLinearData.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
M modules/ve/test/dm/ve.dm.example.js
M modules/ve/ui/dialogs/ve.ui.MWMediaEditDialog.js
M modules/ve/ui/dialogs/ve.ui.MWReferenceDialog.js
M modules/ve/ui/ve.ui.Surface.js
9 files changed, 557 insertions(+), 94 deletions(-)


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

diff --git a/modules/ve/dm/lineardata/ve.dm.MetaLinearData.js 
b/modules/ve/dm/lineardata/ve.dm.MetaLinearData.js
index 32467e6..19bf0f9 100644
--- a/modules/ve/dm/lineardata/ve.dm.MetaLinearData.js
+++ b/modules/ve/dm/lineardata/ve.dm.MetaLinearData.js
@@ -39,13 +39,14 @@
  * @returns {Array} Merged data
  */
 ve.dm.MetaLinearData.static.merge = function ( data ) {
-       var i, merged = [];
+       var i, merged = [], allUndefined = true;
        for ( i = 0; i < data.length; i++ ) {
                if ( data[i] !== undefined ) {
+                       allUndefined = false;
                        merged = merged.concat( data[i] );
                }
        }
-       return [ merged ];
+       return allUndefined ? [ undefined ] : [ merged ];
 };
 
 /* Methods */
diff --git a/modules/ve/dm/ve.dm.Document.js b/modules/ve/dm/ve.dm.Document.js
index 9443022..0ff194f 100644
--- a/modules/ve/dm/ve.dm.Document.js
+++ b/modules/ve/dm/ve.dm.Document.js
@@ -307,7 +307,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 ) {
@@ -322,41 +322,62 @@
                // 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;
 };
 
 /**
  * Get the metadata replace operation required to keep data & metadata in sync 
after a splice
  *
+ * If no metadata replacement is required, this function returns null.
+ *
  * @method
  * @param {number} offset Data offset to start at
  * @param {number} remove Number of elements being removed
  * @param {Array} insert Element data being inserted
- * @returns {Object} Metadata replace operation to keep data & metadata in sync
+ * @param {Array} [insertMeta] Metadata being inserted
+ * @returns {Object|null} Metadata replace operation to keep data & metadata 
in sync
  */
-ve.dm.Document.prototype.getMetadataReplace = function ( offset, remove, 
insert ) {
-       var removeMetadata, insertMetadata, replace = {};
-       if ( remove > insert.length ) {
+ve.dm.Document.prototype.getMetadataReplace = function ( offset, remove, 
insert, insertMeta ) {
+       var i, len, removeMetadata, insertMetadata;
+       if ( insertMeta ) {
+               // Find the first non-empty metadata insertion
+               for ( i = 0, len = insertMeta.length; i < len; i++ ) {
+                       if ( insertMeta[i] && insertMeta[i].length > 0 ) {
+                               return {
+                                       'retain': i,
+                                       'remove': i < remove ?
+                                               this.getMetadata( new ve.Range( 
offset + i, offset + remove + 1 ) ) :
+                                               [],
+                                       'insert': insertMeta.slice( i )
+                               };
+                       }
+               }
+               // No metadata being inserted
+               return null;
+       } else if ( remove > insert.length ) {
                // if we are removing more than we are inserting we need to 
collapse the excess metadata
                removeMetadata = this.getMetadata( new ve.Range( offset + 
insert.length, offset + remove + 1 ) );
                // check removeMetadata is non-empty
                if ( !ve.compare( removeMetadata, new Array( 
removeMetadata.length ) ) ) {
                        insertMetadata = ve.dm.MetaLinearData.static.merge( 
removeMetadata );
-                       replace.retain = insert.length;
-                       replace.remove = removeMetadata;
-                       replace.insert = insertMetadata;
+                       return {
+                               'retain': insert.length,
+                               'remove': removeMetadata,
+                               'insert': insertMetadata
+                       };
                }
        }
        // if insert.length === remove metadata can just stay where it is
-       if ( insert.length > remove ) {
-               // if we are inserting more than we are removing then we need 
to pad out with undefineds
-               replace.retain = remove;
-               replace.insert = new Array( insert.length - remove );
-       }
-       return replace;
+       // if insert.length > remove, then we need to pad out with undefineds, 
but that's the default
+       return null;
 };
 
 /**
diff --git a/modules/ve/dm/ve.dm.InternalList.js 
b/modules/ve/dm/ve.dm.InternalList.js
index 5443632..00c2d6d 100644
--- a/modules/ve/dm/ve.dm.InternalList.js
+++ b/modules/ve/dm/ve.dm.InternalList.js
@@ -370,3 +370,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 3f64305..5406cc2 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;
 };
 
@@ -877,8 +969,9 @@
  * @param {number} offset Offset to start at
  * @param {number} removeLength Number of data items to remove
  * @param {Array} insert Data to insert
+ * @param {Array} [insertMeta] Metadata to insert
  */
-ve.dm.Transaction.prototype.pushReplace = function ( doc, offset, 
removeLength, insert ) {
+ve.dm.Transaction.prototype.pushReplace = function ( doc, offset, 
removeLength, insert, insertMeta ) {
        if ( removeLength === 0 && insert.length === 0 ) {
                // Don't push no-ops
                return;
@@ -887,12 +980,9 @@
        var op, end = this.operations.length - 1,
                lastOp = end >= 0 ? this.operations[end] : null,
                remove = doc.getData( new ve.Range( offset, offset + 
removeLength ) ),
-               metadataReplace = doc.getMetadataReplace( offset, removeLength, 
insert ),
-               retainMetadata = metadataReplace.retain,
-               removeMetadata = metadataReplace.remove,
-               insertMetadata = metadataReplace.insert;
+               metadataReplace = doc.getMetadataReplace( offset, removeLength, 
insert, insertMeta );
 
-       if ( lastOp && lastOp.type === 'replace' && !lastOp.removeMetadata && 
!removeMetadata ) {
+       if ( lastOp && lastOp.type === 'replace' && !lastOp.removeMetadata && 
!metadataReplace ) {
                // simple replaces can just be concatenated
                // TODO: allow replaces with meta to be merged?
                lastOp.insert = lastOp.insert.concat( insert );
@@ -903,10 +993,10 @@
                        'remove': remove,
                        'insert': insert
                };
-               if ( removeMetadata !== undefined ) {
-                       op.retainMetadata = retainMetadata;
-                       op.removeMetadata = removeMetadata;
-                       op.insertMetadata = insertMetadata;
+               if ( metadataReplace ) {
+                       op.retainMetadata = metadataReplace.retain;
+                       op.removeMetadata = metadataReplace.remove;
+                       op.insertMetadata = metadataReplace.insert;
                }
                this.operations.push( op );
        }
diff --git a/modules/ve/test/dm/ve.dm.Transaction.test.js 
b/modules/ve/test/dm/ve.dm.Transaction.test.js
index 4a5c31a..ee53018 100644
--- a/modules/ve/test/dm/ve.dm.Transaction.test.js
+++ b/modules/ve/test/dm/ve.dm.Transaction.test.js
@@ -658,38 +658,263 @@
        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': [
+QUnit.test( 'newFromDocumentInsertion', function ( assert ) {
+       var i, j, doc2, tx, actualStoreItems, expectedStoreItems,
+               doc = ve.dm.example.createExampleDocument( 'internalData' ),
+               complexDoc = ve.dm.example.createExampleDocument( 
'complexInternalData' ),
+               nextIndex = doc.store.valueStore.length,
+               whee = [ { 'type': 'paragraph' }, 'W', 'h', 'e', 'e', { 'type': 
'/paragraph' } ],
+               wheeItem = [ { 'type': 'internalItem' } ].concat( whee 
).concat( [ { 'type': '/internalItem' } ] ),
+               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': '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.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': 7 }
+                               ]
+                       },
+                       {
+                               '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.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': 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 }
+                               ]
+                       },
+                       {
+                               'msg': 'metadata insertion',
+                               'doc': 'complexInternalData',
+                               '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': 'complexInternalData',
+                               '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': 'complexInternalData',
+                               'range': new ve.Range( 7, 7 ),
+                               'newDocData': withReference,
+                               'expectedOps': [
                                        { 'type': 'retain', 'length': 7 },
                                        {
                                                'type': 'replace',
-                                               'remove': doc.data.slice( 7, 12 
),
-                                               'insert': paragraph
+                                               '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': 15 }
-                               ]
-                       },
-                       'replacing second internal node with two paragraphs': {
-                               'args': [doc, secondNode, paragraph.concat( 
paragraph )],
-                               'ops': [
-                                       { 'type': 'retain', 'length': 14 },
+                                       { 'type': 'retain', 'length': 1 },
                                        {
                                                'type': 'replace',
-                                               'remove': 
doc.data.getDataSlice( secondNode.getRange() ),
-                                               'insert': paragraph.concat( 
paragraph )
+                                               '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': 8 }
+                                       { 'type': 'retain', 'length': 1 }
                                ]
                        }
-               };
-       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(),
diff --git a/modules/ve/test/dm/ve.dm.example.js 
b/modules/ve/test/dm/ve.dm.example.js
index 12a5471..74b5763 100644
--- a/modules/ve/test/dm/ve.dm.example.js
+++ b/modules/ve/test/dm/ve.dm.example.js
@@ -110,6 +110,7 @@
        if ( ve.dm.example[name].internalItems ) {
                for ( i = 0; i < ve.dm.example[name].internalItems.length; i++ 
) {
                        doc.internalList.queueItemHtml(
+                               ve.dm.example[name].internalItems[i].group,
                                ve.dm.example[name].internalItems[i].key,
                                ve.dm.example[name].internalItems[i].body
                        );
@@ -398,8 +399,69 @@
 ];
 
 ve.dm.example.internalData.internalItems = [
-       { 'key': 'bar', 'body': 'Bar' },
-       { 'key': 'baz', 'body': 'Baz' }
+       { 'group': 'test', 'key': 'bar', 'body': 'Bar' },
+       { 'group': 'test', 'key': 'baz', 'body': 'Baz' }
+];
+
+ve.dm.example.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.example.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.example.withMeta = [
diff --git a/modules/ve/ui/dialogs/ve.ui.MWMediaEditDialog.js 
b/modules/ve/ui/dialogs/ve.ui.MWMediaEditDialog.js
index 308e0e4..4040eee 100644
--- a/modules/ve/ui/dialogs/ve.ui.MWMediaEditDialog.js
+++ b/modules/ve/ui/dialogs/ve.ui.MWMediaEditDialog.js
@@ -74,7 +74,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.Dialog.prototype.onOpen.call( this );
@@ -82,17 +82,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' );
@@ -104,25 +104,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.Dialog.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/ui/dialogs/ve.ui.MWReferenceDialog.js 
b/modules/ve/ui/dialogs/ve.ui.MWReferenceDialog.js
index 2296f68..2953f01 100644
--- a/modules/ve/ui/dialogs/ve.ui.MWReferenceDialog.js
+++ b/modules/ve/ui/dialogs/ve.ui.MWReferenceDialog.js
@@ -99,7 +99,7 @@
 };
 
 ve.ui.MWReferenceDialog.prototype.onOpen = function () {
-       var focusedNode, data, refGroup, listKey,
+       var focusedNode, newDoc, refGroup, listKey,
                doc = this.surface.getModel().getDocument();
 
        // Parent method
@@ -109,19 +109,19 @@
        focusedNode = this.surface.getView().getFocusedNode();
        if ( focusedNode instanceof ve.ce.MWReferenceNode ) {
                this.internalItem = focusedNode.getModel().getInternalItem();
-               data = doc.getData( this.internalItem.getRange(), true );
+               newDoc = doc.getDocumentSlice( this.internalItem );
                listKey = focusedNode.getModel().getAttribute( 'listKey' );
                refGroup = focusedNode.getModel().getAttribute( 'refGroup' );
        } else {
-               data = [
+               newDoc = [
                        { 'type': 'paragraph', 'internal': { 'generated': 
'wrapper' } },
-                       { 'type': '/paragraph' }
+                       { 'type': '/paragraph' },
+                       { 'type': 'internalList' },
+                       { 'type': '/internalList' }
                ];
        }
 
-       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.$$ } );
 
        // Initialization
@@ -136,7 +136,7 @@
 };
 
 ve.ui.MWReferenceDialog.prototype.onClose = function ( action ) {
-       var data, doc, listIndex, listGroup, listKey, refGroup, newItem, 
refNode, oldListGroup,
+       var newDoc, doc, listIndex, listGroup, listKey, refGroup, newItem, 
refNode, oldListGroup,
                oldListKey, oldNodes, internalList, attrChanges,
                surfaceModel = this.surface.getModel();
 
@@ -145,7 +145,7 @@
 
        // Save changes
        if ( action === 'apply' ) {
-               data = this.referenceSurface.getModel().getDocument().getData();
+               newDoc = this.referenceSurface.getModel().getDocument();
                doc = surfaceModel.getDocument();
                listKey = this.nameInput.getValue() !== '' ? 
this.nameInput.getValue() : null;
                refGroup = this.groupInput.getValue();
@@ -166,7 +166,9 @@
                                        attrChanges.listIndex = listIndex;
                                } else if ( oldNodes.length > 1 ) {
                                        // If the old internal node was being 
shared, create a new one
-                                       newItem = 
internalList.getItemInsertion( listGroup, listKey, data );
+                                       // FIXME this doesn't handle metadata 
and internal items the way
+                                       // newFromDocumentInsertion does
+                                       newItem = 
internalList.getItemInsertion( listGroup, listKey, newDoc.getData() );
                                        attrChanges.listIndex = newItem.index;
                                }
                                attrChanges.listGroup = listGroup;
@@ -185,14 +187,16 @@
                        // Process the internal node create/edit transaction
                        if ( !newItem ) {
                                surfaceModel.change(
-                                       
ve.dm.Transaction.newFromNodeReplacement( doc, this.internalItem, data )
+                                       
ve.dm.Transaction.newFromDocumentInsertion( doc, this.internalItem, newDoc )
                                );
                        } else {
                                surfaceModel.change( newItem.transaction );
                        }
                } else {
                        // Create reference: just create new nodes
-                       newItem = doc.getInternalList().getItemInsertion( 
listGroup, listKey, data );
+                       // FIXME this doesn't handle metadata and internal 
items the way
+                       // newFromDocumentInsertion does
+                       newItem = doc.getInternalList().getItemInsertion( 
listGroup, listKey, newDoc.getData() );
                        surfaceModel.change( newItem.transaction );
                        
surfaceModel.getFragment().collapseRangeToEnd().insertContent( [
                                {
diff --git a/modules/ve/ui/ve.ui.Surface.js b/modules/ve/ui/ve.ui.Surface.js
index 780ab10..6ead14d 100644
--- a/modules/ve/ui/ve.ui.Surface.js
+++ b/modules/ve/ui/ve.ui.Surface.js
@@ -12,10 +12,10 @@
  * @extends ve.Element
  *
  * @constructor
- * @param {HTMLDocument|Array|ve.dm.LinearData} data Document data to edit
+ * @param {HTMLDocument|Array|ve.dm.LinearData|ve.dm.Document} dataOrDocument 
Document data to edit
  * @param {Object} [config] Config options
  */
-ve.ui.Surface = function VeUiSurface( data, config ) {
+ve.ui.Surface = function VeUiSurface( dataOrDocument, config ) {
        // Parent constructor
        ve.Element.call( this, config );
 
@@ -25,7 +25,10 @@
        // Properties
        this.$globalOverlay = $( '<div>' );
        this.$localOverlay = this.$$( '<div>' );
-       this.model = new ve.dm.Surface( new ve.dm.Document( data ) );
+       this.model = new ve.dm.Surface(
+               dataOrDocument instanceof ve.dm.Document ?
+               dataOrDocument : new ve.dm.Document( dataOrDocument )
+       );
        this.view = new ve.ce.Surface( this.model, this, { '$$': this.$$ } );
        this.context = new ve.ui.Context( this, { '$$': this.$$ } );
        this.dialogs = new ve.ui.WindowSet( this, ve.ui.dialogFactory );

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I48bf5b8aef02a51947a2c3ebcfe2fa7afe90a31a
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