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