jenkins-bot has submitted this change and it was merged.
Change subject: Introduce newFromDocumentReplace() transaction builder
......................................................................
Introduce newFromDocumentReplace() transaction builder
Replaces newFromNodeReplacement(). newFromNodeReplacement was very
simplistic and didn't support metadata or internal list items, so
if you had comments or references inside of the data you were editing
(reference contents or an image caption), they'd get mangled.
With this, you can do:
newDoc = doc.getDocumentSlice( node );
// Edit newDoc
tx = ve.dm.Transaction.newFromDocumentReplace( doc, node, newDoc );
surface.change( newDoc );
and that takes care of metadata, internal list items, and things like
references that reference internal list items.
ve.dm.Document.js:
* In getDocumentSlice(), store a reference to the original document
and the number of items in its InternalList at the time of slicing
in the created slice. This is used for reconciliation when the
modified slice is injected back into the parent document with
newFromDocumentReplace().
ve.dm.InternalList.js:
* Add a method for merging in another InternalList. This provides a
mapping from old to new InternalList indexes so the linear model data
being injected by newFromDocumentReplace() can have its InternalList
indexes remapped.
ve.dm.Transaction.js:
* Replace newFromNodeReplacement() with newFromDocumentReplace()
ve.ui.MWMediaEditDialog.js, ve.ui.MWReferenceDialog.js:
* Use getDocumentSlice/newFromDocumentReplace for editing captions/refs
* Change insertion code path to insert an empty internalItem/caption, then
newFromDocumentReplace into that
* Add empty internalList to new mini-documents
ve/test/dm/ve.dm.Transaction.test.js:
* Replace newFromNodeReplacement tests with newFromDocumentReplace tests
ve-mw/test/dm/ve.dm.Transaction.test.js (new):
* Add tests for newFromDocumentReplace with mwReference nodes
ve.dm.mwExample.js:
* Add data for newFromDocumentReplace with mwReference tests
VisualEditor.hooks.php:
* Add new test file
Bug: 52102
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/ui/dialogs/ve.ui.MWMediaEditDialog.js
M modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.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
9 files changed, 550 insertions(+), 68 deletions(-)
Approvals:
Esanders: Looks good to me, approved
jenkins-bot: Verified
diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index d505def..0e87f07 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -341,6 +341,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..c38b7c1
--- /dev/null
+++ b/modules/ve-mw/test/dm/ve.dm.Transaction.test.js
@@ -0,0 +1,145 @@
+/*!
+ * 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( 'newFromDocumentReplace 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 ) ),
+ 'removeMetadata':
complexDoc.getMetadata( new ve.Range( 0, 7 ) ),
+ '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 ) ),
+ 'removeMetadata':
complexDoc.getMetadata( new ve.Range( 8, 32 ) ),
+ 'insertMetadata':
complexDoc.getMetadata( new ve.Range( 8, 32 ) )
+ },
+ { '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 ) ),
+ 'removeMetadata':
complexDoc.getMetadata( new ve.Range( 8, 32 ) ),
+ 'insertMetadata':
complexDoc.getMetadata( new ve.Range( 8, 30 ) )
+ .concat( [ [] ] )
+ .concat(
complexDoc.getMetadata( new ve.Range( 31, 32 ) ) )
+ },
+ { '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 ) ),
+ 'removeMetadata':
complexDoc.getMetadata( new ve.Range( 8, 32 ) ),
+ 'insertMetadata':
complexDoc.getMetadata( new ve.Range( 8, 32 ) )
+ .concat( new Array( 7 )
)
+ },
+ { 'type': 'retain', 'length': 1 }
+ ]
+ }
+ ];
+ QUnit.expect( 2 * cases.length );
+ for ( i = 0; i < cases.length; i++ ) {
+ doc = cases[i].doc; // TODO deep copy?
+ 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.newFromDocumentReplace( 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 adb0570..bd663ef 100644
--- a/modules/ve-mw/test/dm/ve.dm.mwExample.js
+++ b/modules/ve-mw/test/dm/ve.dm.mwExample.js
@@ -570,6 +570,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 = {
'adjacent annotations': {
'html':
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
index 6467a55..6bfd55e 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
@@ -20,6 +20,7 @@
ve.ui.MWDialog.call( this, surface, config );
// Properties
+ this.mediaNode = null;
this.captionNode = null;
};
@@ -87,24 +88,27 @@
/** */
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 );
// Properties
- this.captionNode =
this.surface.getView().getFocusedNode().getModel().getCaptionNode();
+ this.mediaNode = this.surface.getView().getFocusedNode().getModel();
+ this.captionNode = this.mediaNode.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.SurfaceWidget(
- new ve.dm.ElementLinearData( doc.getStore(), data ),
+ newDoc,
{
'$$': this.frame.$$,
'tools': this.constructor.static.toolbarGroups,
@@ -119,28 +123,26 @@
/** */
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.getContent();
+ newDoc =
this.captionSurface.getSurface().getModel().getDocument();
doc = surfaceModel.getDocument();
- if ( this.captionNode ) {
- // Replace the contents of the caption
- surfaceModel.getFragment( this.captionNode.getRange(),
true ).insertContent( data );
- } else {
+ if ( !this.captionNode ) {
// Insert a new caption at the beginning of the image
node
surfaceModel.getFragment()
.adjustRange( 1 )
.collapseRangeToStart()
- .insertContent(
- [ { 'type': 'mwImageCaption' } ]
- .concat( data )
- .concat( [ { 'type':
'/mwImageCaption' } ] )
- );
+ .insertContent( [ { 'type': 'mwImageCaption' },
{ 'type': '/mwImageCaption' } ] );
+ this.captionNode = this.mediaNode.getCaptionNode();
}
+ // Replace the contents of the caption
+ surfaceModel.change(
+ ve.dm.Transaction.newFromDocumentReplace( doc,
this.captionNode, newDoc )
+ );
}
// Cleanup
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
index 7030907..0405683 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
@@ -170,7 +170,7 @@
* @inheritdoc
*/
ve.ui.MWReferenceDialog.prototype.onClose = function ( action ) {
- var i, len, txs, item, data, group, refGroup, listGroup, keyIndex,
refNodes,
+ var i, len, txs, item, newDoc, group, refGroup, listGroup, keyIndex,
refNodes,
surfaceModel = this.surface.getModel(),
// Store the original selection browsers may reset it after
// the first model change.
@@ -182,7 +182,7 @@
ve.ui.MWDialog.prototype.onClose.call( this, action );
if ( action === 'insert' || action === 'apply' ) {
- data = this.referenceSurface.getContent();
+ newDoc =
this.referenceSurface.getSurface().getModel().getDocument();
refGroup = this.referenceGroupInput.getValue();
listGroup = 'mwReference/' + refGroup;
@@ -223,8 +223,8 @@
}
// Update internal node content
surfaceModel.change(
- ve.dm.Transaction.newFromNodeReplacement(
- doc, internalList.getItemNode(
this.ref.listIndex ), data
+ ve.dm.Transaction.newFromDocumentReplace(
+ doc, internalList.getItemNode(
this.ref.listIndex ), newDoc
)
);
}
@@ -239,9 +239,15 @@
'listGroup': listGroup,
'refGroup': refGroup
};
- item = internalList.getItemInsertion(
this.ref.listGroup, this.ref.listKey, data );
+ // Insert an internal item, then inject the
subdocument into it
+ item = internalList.getItemInsertion(
this.ref.listGroup, this.ref.listKey, [] );
surfaceModel.change( item.transaction );
this.ref.listIndex = item.index;
+ surfaceModel.change(
+
ve.dm.Transaction.newFromDocumentReplace(
+ doc, internalList.getItemNode(
this.ref.listIndex ), newDoc
+ )
+ );
}
// Add reference at cursor
surfaceModel.getFragment( selection
).collapseRangeToEnd().insertContent( [
@@ -262,7 +268,7 @@
* @chainable
*/
ve.ui.MWReferenceDialog.prototype.useReference = function ( ref ) {
- var data, refGroup,
+ var newDoc, refGroup,
doc = this.surface.getModel().getDocument();
if ( ref ) {
@@ -273,15 +279,17 @@
'refGroup': ref.refGroup,
'listIndex': ref.listIndex
};
- data = doc.getData( doc.getInternalList().getItemNode(
ref.listIndex ).getRange(), true );
+ newDoc = doc.getDocumentSlice(
doc.getInternalList().getItemNode( ref.listIndex ) );
refGroup = ref.refGroup;
} else {
// Create a new reference
this.ref = null;
- data = [
+ newDoc = new ve.dm.Document( [
{ 'type': 'paragraph', 'internal': { 'generated':
'wrapper' } },
- { 'type': '/paragraph' }
- ];
+ { 'type': '/paragraph' },
+ { 'type': 'internalList' },
+ { 'type': '/internalList' }
+ ] );
refGroup = '';
}
@@ -292,7 +300,7 @@
// Properties
this.referenceSurface = new ve.ui.SurfaceWidget(
- new ve.dm.ElementLinearData( doc.getStore(), data ),
+ newDoc,
{
'$$': this.frame.$$,
'tools': this.constructor.static.toolbarGroups,
diff --git a/modules/ve/dm/ve.dm.Document.js b/modules/ve/dm/ve.dm.Document.js
index daa30ac..d71e9c1 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 e69a969..05966d8 100644
--- a/modules/ve/dm/ve.dm.InternalList.js
+++ b/modules/ve/dm/ve.dm.InternalList.js
@@ -410,3 +410,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 cfbc85e..a94f188 100644
--- a/modules/ve/dm/ve.dm.Transaction.js
+++ b/modules/ve/dm/ve.dm.Transaction.js
@@ -152,27 +152,122 @@
};
/**
- * 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} removeNodeOrRange Node or range to remove
+ * @param {ve.dm.Document} newDoc Document to insert
+ * @returns {ve.dm.Transaction} Transaction that replaces the node and updates
the internal list
+ * @throws {Error} removeNodeOrRange must be a ve.dm.Node or a ve.Range
*/
-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();
+ve.dm.Transaction.newFromDocumentReplace = function ( doc, removeNodeOrRange,
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 ( removeNodeOrRange instanceof ve.dm.Node ) {
+ range = removeNodeOrRange.getRange();
+ } else if ( removeNodeOrRange instanceof ve.Range ) {
+ range = removeNodeOrRange;
+ } else {
+ throw new Error( 'removeNodeOrRange must be a ve.dm.Node or a
ve.Range' );
}
- if ( !( range instanceof ve.Range ) ) {
- throw new Error( 'nodeOrRange must be a ve.dm.Node or a
ve.Range' );
+
+ // 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.copy( 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.
+ if ( newDoc.origInternalListLength > 0 ) {
+ oldEndOffset = doc.internalList.getItemNode(
newDoc.origInternalListLength - 1 ).getOuterRange().end;
+ newEndOffset = newDoc.internalList.getItemNode(
newDoc.origInternalListLength - 1 ).getOuterRange().end;
+ } else {
+ oldEndOffset = listNodeRange.start;
+ newEndOffset = newListNodeRange.start;
+ }
+ 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 );
}
- tx.pushRetain( range.start );
- tx.pushReplace( doc, range.start, range.end - range.start, newData );
- tx.pushFinalRetain( doc, range.end );
+ 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 ) );
+ }
+
+ 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 9e3851f..5e09cf2 100644
--- a/modules/ve/test/dm/ve.dm.Transaction.test.js
+++ b/modules/ve/test/dm/ve.dm.Transaction.test.js
@@ -668,38 +668,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( 'newFromDocumentReplace', 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.newFromDocumentReplace( 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: merged
Gerrit-Change-Id: I4aa980780114b391924f04df588e81c990c32983
Gerrit-PatchSet: 11
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Catrope <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Cscott <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits