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