Merge changes I4b62a310,I7c9f22a1

* changes:
  Add mutators in MetaList
  Move .commit()/.rollback() from TransactionProcessor to Document
This commit is contained in:
jenkins-bot 2013-03-26 14:31:44 +00:00 committed by Gerrit Code Review
commit 34cc39bf9f
8 changed files with 174 additions and 96 deletions

View file

@ -449,20 +449,32 @@ ve.dm.Document.getDataSlice = function ( sourceData, range, deep ) {
* Reverse a transaction's effects on the content data.
*
* @method
* @param {ve.dm.Transaction}
* @param {ve.dm.Transaction} transaction Transaction to roll back
* @emits transact
* @throws {Error} Cannot roll back a transaction that has not been committed
*/
ve.dm.Document.prototype.rollback = function ( transaction ) {
ve.dm.TransactionProcessor.rollback( this, transaction );
if ( !transaction.hasBeenApplied() ) {
throw new Error( 'Cannot roll back a transaction that has not been committed' );
}
new ve.dm.TransactionProcessor( this, transaction, true ).process();
this.emit( 'transact', transaction, true );
};
/**
* Apply a transaction's effects on the content data.
*
* @method
* @param {ve.dm.Transaction}
* @param {ve.dm.Transaction} transaction Transaction to apply
* @emits transact
* @throws {Error} Cannot commit a transaction that has already been committed
*/
ve.dm.Document.prototype.commit = function ( transaction ) {
ve.dm.TransactionProcessor.commit( this, transaction );
if ( transaction.hasBeenApplied() ) {
throw new Error( 'Cannot commit a transaction that has already been committed' );
}
new ve.dm.TransactionProcessor( this, transaction, false ).process();
this.emit( 'transact', transaction, false );
};
/**

View file

@ -162,6 +162,17 @@ ve.dm.MetaItem.static.storeHtmlAttributes = true;
/* Methods */
/**
* Remove this item from the document. Only works if the item is attached to a MetaList.
* @throws {Error} Cannot remove detached item
*/
ve.dm.MetaItem.prototype.remove = function () {
if ( !this.list ) {
throw new Error( 'Cannot remove detached item' );
}
this.list.removeMeta( this );
};
/**
* Get the group this meta item belongs to.
* @see ve.dm.MetaItem#static.group

View file

@ -11,15 +11,16 @@
* @class
* @extends ve.EventEmitter
* @constructor
* @param {ve.dm.Document} doc Document
* @param {ve.dm.Surface} surface Surface model
*/
ve.dm.MetaList = function VeDmMetaList( doc ) {
ve.dm.MetaList = function VeDmMetaList( surface ) {
var i, j, jlen, metadata, item, group;
// Parent constructor
ve.EventEmitter.call( this );
// Properties
this.document = doc;
this.surface = surface;
this.document = surface.getDocument();
this.groups = {};
this.items = [];
@ -84,12 +85,12 @@ ve.dm.MetaList.prototype.onTransact = function ( tx, reversed ) {
ins = reversed ? ops[i].remove : ops[i].insert;
rm = reversed ? ops[i].insert : ops[i].remove;
for ( j = 0, jlen = rm.length; j < jlen; j++ ) {
this.removeItem( offset, index + j );
this.deleteRemovedItem( offset, index + j );
}
for ( j = 0, jlen = ins.length; j < jlen; j++ ) {
item = ve.dm.metaItemFactory.createFromElement( ins[j] );
// offset and index are pre-transaction, but we'll fix them later
this.insertItem( offset, index + j, item );
this.addInsertedItem( offset, index + j, item );
}
index += rm.length;
break;
@ -184,18 +185,51 @@ ve.dm.MetaList.prototype.getAllItems = function () {
return this.items.slice( 0 );
};
/**
* Insert new metadata into the document. This builds and processes a transaction that inserts
* metadata into the document.
* @param {Object|ve.dm.MetaItem} meta Metadata element (or MetaItem) to insert
* @param {Number} offset Offset at which to insert the new metadata
* @param {Number} [index] Index at which to insert the new metadata, or undefined to add to the end
*/
ve.dm.MetaList.prototype.insertMeta = function ( meta, offset, index ) {
var tx;
if ( meta instanceof ve.dm.MetaItem ) {
meta = meta.getElement();
}
if ( index === undefined ) {
index = ( this.document.metadata[offset] || [] ).length;
}
tx = ve.dm.Transaction.newFromMetadataInsertion( this.document, offset, index, [ meta ] );
this.surface.change( tx );
};
/**
* Remove a meta item from the document. This builds and processes a transaction that removes the
* associated metadata from the document.
* @param {ve.dm.MetaItem} item Item to remove
*/
ve.dm.MetaList.prototype.removeMeta = function ( item ) {
var tx;
tx = ve.dm.Transaction.newFromMetadataRemoval(
this.document,
item.getOffset(),
new ve.Range( item.getIndex(), item.getIndex() + 1 )
);
this.surface.change( tx );
};
/**
* Insert an item at a given offset and index in response to a transaction.
*
* This function is for internal usage by onTransact(). To actually insert an item, you need to
* process a transaction against the document that inserts metadata, then the MetaList will
* automatically update itself and add the item.
* This function is for internal usage by onTransact(). To actually insert an item, use
* insertItem().
*
* @param {number} offset Offset in the linear model of the new item
* @param {number} index Index of the new item in the metadata array at offset
* @param {ve.dm.MetaItem} item Item object
*/
ve.dm.MetaList.prototype.insertItem = function ( offset, index, item ) {
ve.dm.MetaList.prototype.addInsertedItem = function ( offset, index, item ) {
var group = item.getGroup(), at = this.findItem( offset, index, null, true );
this.items.splice( at, 0, item );
if ( this.groups[group] ) {
@ -210,14 +244,13 @@ ve.dm.MetaList.prototype.insertItem = function ( offset, index, item ) {
/**
* Remove an item in response to a transaction.
*
* This function is for internal usage by onTransact(). To actually remove an item, you need to
* process a transaction against the document that removes the associated metadata, then the
* MetaList will automatically update itself and remove the item.
* This function is for internal usage by onTransact(). To actually remove an item, use
* removeItem().
*
* @param {number} offset Offset in the linear model of the item
* @param {number} index Index of the item in the metadata array at offset
*/
ve.dm.MetaList.prototype.removeItem = function ( offset, index ) {
ve.dm.MetaList.prototype.deleteRemovedItem = function ( offset, index ) {
var item, group, at = this.findItem( offset, index );
if ( at === null ) {
return;

View file

@ -285,7 +285,7 @@ ve.dm.Surface.prototype.change = function ( transactions, selection ) {
this.bigStack = this.bigStack.slice( 0, this.bigStack.length - this.undoIndex );
this.undoIndex = 0;
this.smallStack.push( transactions[i] );
ve.dm.TransactionProcessor.commit( this.documentModel, transactions[i] );
this.documentModel.commit( transactions[i] );
}
}
}

View file

@ -9,8 +9,7 @@
* DataModel transaction processor.
*
* This class reads operations from a transaction and applies them one by one. It's not intended
* to be used directly; use the static functions ve.dm.TransactionProcessor.commit() and .rollback()
* instead.
* to be used directly; use the .commit() and .rollback() methods of ve.dm.Document.
*
* NOTE: Instances of this class are not recyclable: you can only call .process() on them once.
*
@ -42,40 +41,6 @@ ve.dm.TransactionProcessor = function VeDmTransactionProcessor( doc, transaction
/* See ve.dm.TransactionProcessor.processors */
ve.dm.TransactionProcessor.processors = {};
/* Static methods */
/**
* Commit a transaction to a document.
*
* @static
* @method
* @param {ve.dm.Document} doc Document object to apply the transaction to
* @param {ve.dm.Transaction} transaction Transaction to apply
*/
ve.dm.TransactionProcessor.commit = function ( doc, transaction ) {
if ( transaction.hasBeenApplied() ) {
throw new Error( 'Cannot commit a transaction that has already been committed' );
}
new ve.dm.TransactionProcessor( doc, transaction, false ).process();
};
/**
* Roll back a transaction.
*
* This applies the transaction to the document in reverse.
*
* @static
* @method
* @param {ve.dm.Document} doc Document object to apply the transaction to
* @param {ve.dm.Transaction} transaction Transaction to apply
*/
ve.dm.TransactionProcessor.rollback = function ( doc, transaction ) {
if ( !transaction.hasBeenApplied() ) {
throw new Error( 'Cannot roll back a transaction that has not been committed' );
}
new ve.dm.TransactionProcessor( doc, transaction, true ).process();
};
/* Methods */
/**
@ -144,8 +109,6 @@ ve.dm.TransactionProcessor.prototype.process = function () {
}
// Mark the transaction as committed or rolled back, as appropriate
this.transaction.toggleApplied();
// Emit an event on the document
this.document.emit( 'transact', this.transaction, this.reversed );
};
/**

View file

@ -1532,4 +1532,34 @@ QUnit.test( 'getSlice', function ( assert ) {
cases[i].msg
);
}
} );
QUnit.test( 'protection against double application of transactions', 3, function ( assert ) {
var tx = new ve.dm.Transaction(),
testDocument = new ve.dm.Document( ve.dm.example.data );
tx.pushRetain( 1 );
tx.pushReplace( [], ['H', 'e', 'l', 'l', 'o' ] );
assert.throws(
function () {
testDocument.rollback( tx );
},
Error,
'exception thrown when trying to rollback an uncommitted transaction'
);
testDocument.commit( tx );
assert.throws(
function () {
testDocument.commit( tx );
},
Error,
'exception thrown when trying to commit an already-committed transaction'
);
testDocument.rollback( tx );
assert.throws(
function () {
testDocument.rollback( tx );
},
Error,
'exception thrown when trying to roll back a transaction that has already been rolled back'
);
} );

View file

@ -30,7 +30,8 @@ function assertItemsMatchMetadata( assert, metadata, list, msg, full ) {
QUnit.test( 'constructor', function ( assert ) {
var i, n = 0,
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
list = new ve.dm.MetaList( doc ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface ),
metadata = doc.metadata;
for ( i in metadata ) {
if ( ve.isArray( metadata[i] ) ) {
@ -42,7 +43,7 @@ QUnit.test( 'constructor', function ( assert ) {
} );
QUnit.test( 'onTransact', function ( assert ) {
var i, j, tx, list, n = 0,
var i, j, surface, tx, list, n = 0,
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
comment = { 'type': 'alienMeta', 'attributes': { 'style': 'comment', 'text': 'onTransact test' } },
heading = { 'type': 'heading', 'attributes': { 'level': 2 } },
@ -134,10 +135,12 @@ QUnit.test( 'onTransact', function ( assert ) {
tx[cases[i].calls[j][0]].apply( tx, cases[i].calls[j].slice( 1 ) );
}
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) );
list = new ve.dm.MetaList( doc );
ve.dm.TransactionProcessor.commit( doc, tx );
surface = new ve.dm.Surface( doc );
list = new ve.dm.MetaList( surface );
// Test both the transaction-via-surface and transaction-via-document flows
surface.change( tx );
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg, false );
ve.dm.TransactionProcessor.rollback( doc, tx );
doc.rollback( tx );
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg + ' (rollback)', false );
}
} );
@ -147,8 +150,9 @@ QUnit.test( 'findItem', function ( assert ) {
n = 0,
groups = [ null ],
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
surface = new ve.dm.Surface( doc ),
metadata = doc.metadata,
list = new ve.dm.MetaList( doc );
list = new ve.dm.MetaList( surface );
for ( i = 0; i < metadata.length; i++ ) {
if ( ve.isArray( metadata[i] ) ) {
@ -180,4 +184,60 @@ QUnit.test( 'findItem', function ( assert ) {
assert.strictEqual( list.findItem( i, j, groups[g], true ), item + 1, groupDesc + ' (forInsertion) (' + i + ', ' + j + ')' );
}
}
} );
QUnit.test( 'insertMeta', 5, function ( assert ) {
var expected,
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface ),
insert = {
'type': 'alienMeta',
'attributes': {
'style': 'comment',
'text': 'insertMeta test'
}
};
list.insertMeta( insert, 2, 0 );
assert.deepEqual( doc.metadata[2], [ insert ], 'Inserting metadata at an offset without pre-existing metadata' );
expected = doc.metadata[0].slice( 0 );
expected.splice( 1, 0, insert );
list.insertMeta( insert, 0, 1 );
assert.deepEqual( doc.metadata[0], expected, 'Inserting metadata in the middle' );
expected.push( insert );
list.insertMeta( insert, 0 );
assert.deepEqual( doc.metadata[0], expected, 'Inserting metadata without passing an index adds to the end' );
list.insertMeta( insert, 1 );
assert.deepEqual( doc.metadata[1], [ insert ], 'Inserting metadata without passing an index without pre-existing metadata' );
list.insertMeta( new ve.dm.AlienMetaItem( insert ), 1 );
assert.deepEqual( doc.metadata[1], [ insert, insert ], 'Passing a MetaItem rather than an element' );
} );
QUnit.test( 'removeMeta', 4, function ( assert ) {
var expected,
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface );
list.removeMeta( list.getItemAt( 4, 0 ) );
assert.deepEqual( doc.metadata[4], [], 'Removing the only item at offset 4' );
expected = doc.metadata[0].slice( 0 );
expected.splice( 1, 1 );
list.removeMeta( list.getItemAt( 0, 1 ) );
assert.deepEqual( doc.metadata[0], expected, 'Removing the item at (0,1)' );
expected = doc.metadata[11].slice( 0 );
expected.splice( 0, 1 );
list.getItemAt( 11, 0 ).remove();
assert.deepEqual( doc.metadata[11], expected, 'Removing (11,0) using .remove()' );
expected.splice( 1, 1 );
list.getItemAt( 11, 1 ).remove();
assert.deepEqual( doc.metadata[11], expected, 'Removing (11,1) (formerly (11,2)) using .remove()' );
} );

View file

@ -9,37 +9,6 @@ QUnit.module( 've.dm.TransactionProcessor' );
/* Tests */
QUnit.test( 'protection against double application', 3, function ( assert ) {
var tx,
testDocument = new ve.dm.Document( ve.dm.example.data );
tx = new ve.dm.Transaction();
tx.pushRetain( 1 );
tx.pushReplace( [], ['H', 'e', 'l', 'l', 'o' ] );
assert.throws(
function () {
ve.dm.TransactionProcessor.rollback( testDocument, tx );
},
Error,
'exception thrown when trying to rollback an uncommitted transaction'
);
ve.dm.TransactionProcessor.commit( testDocument, tx );
assert.throws(
function () {
ve.dm.TransactionProcessor.commit( testDocument, tx );
},
Error,
'exception thrown when trying to commit an already-committed transaction'
);
ve.dm.TransactionProcessor.rollback( testDocument, tx );
assert.throws(
function () {
ve.dm.TransactionProcessor.rollback( testDocument, tx );
},
Error,
'exception thrown when trying to roll back a transaction that has already been rolled back'
);
} );
QUnit.test( 'commit/rollback', 86, function ( assert ) {
var i, key, originalData, originalDoc, msg, testDocument, tx,
expectedData, expectedDocument,
@ -396,7 +365,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
cases[msg].expected( expectedData );
expectedDocument = new ve.dm.Document( ve.copyArray ( expectedData ) );
// Commit
ve.dm.TransactionProcessor.commit( testDocument, tx );
testDocument.commit( tx );
assert.deepEqual( testDocument.getFullData(), expectedData, 'commit (data): ' + msg );
assert.equalNodeTree(
testDocument.getDocumentNode(),
@ -404,7 +373,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
'commit (tree): ' + msg
);
// Rollback
ve.dm.TransactionProcessor.rollback( testDocument, tx );
testDocument.rollback( tx );
assert.deepEqual( testDocument.getFullData(), originalData, 'rollback (data): ' + msg );
assert.equalNodeTree(
testDocument.getDocumentNode(),
@ -415,7 +384,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
/*jshint loopfunc:true */
assert.throws(
function () {
ve.dm.TransactionProcessor.commit( testDocument, tx );
testDocument.commit( tx );
},
cases[msg].exception,
'commit: ' + msg