https://www.mediawiki.org/wiki/Special:Code/MediaWiki/103516

Revision: 103516
Author:   tparscal
Date:     2011-11-17 22:42:18 +0000 (Thu, 17 Nov 2011)
Log Message:
-----------
Renamed es.Transaction to es.TransactionModel

Modified Paths:
--------------
    trunk/extensions/VisualEditor/demo/index.html
    trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js
    trunk/extensions/VisualEditor/tests/es/index.html

Added Paths:
-----------
    trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
    trunk/extensions/VisualEditor/modules/es/models/es.TransactionModel.js
    trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js

Removed Paths:
-------------
    trunk/extensions/VisualEditor/modules/es/es.Transaction.js
    trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
    trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js

Modified: trunk/extensions/VisualEditor/demo/index.html
===================================================================
--- trunk/extensions/VisualEditor/demo/index.html       2011-11-17 22:16:22 UTC 
(rev 103515)
+++ trunk/extensions/VisualEditor/demo/index.html       2011-11-17 22:42:18 UTC 
(rev 103516)
@@ -58,7 +58,6 @@
                <script src="../modules/es/es.Html.js"></script>
                <script src="../modules/es/es.Position.js"></script>
                <script src="../modules/es/es.Range.js"></script>
-               <script src="../modules/es/es.Transaction.js"></script>
                <script src="../modules/es/es.TransactionProcessor.js"></script>
 
                <!-- Serializers -->
@@ -90,6 +89,7 @@
                <script src="../modules/es/models/es.TableRowModel.js"></script>
                <script 
src="../modules/es/models/es.TableCellModel.js"></script>
                <script src="../modules/es/models/es.HeadingModel.js"></script>
+               <script 
src="../modules/es/models/es.TransactionModel.js"></script>
 
                <!-- Views -->
                <script src="../modules/es/views/es.SurfaceView.js"></script>

Deleted: trunk/extensions/VisualEditor/modules/es/es.Transaction.js
===================================================================
--- trunk/extensions/VisualEditor/modules/es/es.Transaction.js  2011-11-17 
22:16:22 UTC (rev 103515)
+++ trunk/extensions/VisualEditor/modules/es/es.Transaction.js  2011-11-17 
22:42:18 UTC (rev 103516)
@@ -1,137 +0,0 @@
-/**
- * Creates an es.Transaction object.
- * 
- * @class
- * @constructor
- * @param {Object[]} operations List of operations
- */
-es.Transaction = function( operations ) {
-       this.operations = es.isArray( operations ) ? operations : [];
-};
-
-/* Methods */
-
-/**
- * Gets a list of all operations.
- * 
- * @method
- * @returns {Object[]} List of operations
- */
-es.Transaction.prototype.getOperations = function() {
-       return this.operations;
-};
-
-/**
- * Merges consecutive operations of the same type.
- * 
- * @method
- */
-es.Transaction.prototype.optimize = function() {
-       for ( var i = 0; i < this.operations.length - 1; i++ ) {
-               var a = this.operations[i];
-               var b = this.operations[i + 1];
-               if ( a.type === b.type ) {
-                       switch ( a.type ) {
-                               case 'retain':
-                                       a.length += b.length;
-                                       this.operations.splice( i + 1, 1 );
-                                       i--;
-                                       break;
-                               case 'insert':
-                               case 'remove':
-                                       a.data = a.data.concat( b.data );
-                                       this.operations.splice( i + 1, 1 );
-                                       i--;
-                                       break;
-                       }
-               }
-       }
-};
-
-/**
- * Adds a retain operation.
- * 
- * @method
- * @param {Integer} length Length of content data to retain
- */
-es.Transaction.prototype.pushRetain = function( length ) {
-       this.operations.push( {
-               'type': 'retain',
-               'length': length
-       } );
-};
-
-/**
- * Adds an insertion operation.
- * 
- * @method
- * @param {Array} data Data to retain
- */
-es.Transaction.prototype.pushInsert = function( data ) {
-       this.operations.push( {
-               'type': 'insert',
-               'data': data
-       } );
-};
-
-/**
- * Adds a removal operation.
- * 
- * @method
- * @param {Array} data Data to remove
- */
-es.Transaction.prototype.pushRemove = function( data ) {
-       this.operations.push( {
-               'type': 'remove',
-               'data': data
-       } );
-};
-
-/**
- * Adds an element attribute change operation.
- * 
- * @method
- * @param {String} method Method to use, either "set" or "clear"
- * @param {String} key Name of attribute to change
- * @param {Mixed} value Value to set attribute to, or value of attribute being 
cleared
- */
-es.Transaction.prototype.pushChangeElementAttribute = function( method, key, 
value ) {
-       this.operations.push( {
-               'type': 'attribute',
-               'method': method,
-               'key': key,
-               'value': value
-       } );
-};
-
-/**
- * Adds a start annotating operation.
- * 
- * @method
- * @param {String} method Method to use, either "set" or "clear"
- * @param {Object} annotation Annotation object to start setting or clearing 
from content data
- */
-es.Transaction.prototype.pushStartAnnotating = function( method, annotation ) {
-       this.operations.push( {
-               'type': 'annotate',
-               'method': method,
-               'bias': 'start',
-               'annotation': annotation
-       } );
-};
-
-/**
- * Adds a stop annotating operation.
- * 
- * @method
- * @param {String} method Method to use, either "set" or "clear"
- * @param {Object} annotation Annotation object to stop setting or clearing 
from content data
- */
-es.Transaction.prototype.pushStopAnnotating = function( method, annotation ) {
-       this.operations.push( {
-               'type': 'annotate',
-               'method': method,
-               'bias': 'stop',
-               'annotation': annotation
-       } );
-};

Deleted: trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
===================================================================
--- trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js 
2011-11-17 22:16:22 UTC (rev 103515)
+++ trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js 
2011-11-17 22:42:18 UTC (rev 103516)
@@ -1,341 +0,0 @@
-/**
- * Creates an es.TransactionProcessor object.
- * 
- * @class
- * @constructor
- */
-es.TransactionProcessor = function( model, transaction ) {
-       this.model = model;
-       this.transaction = transaction;
-       this.cursor = 0;
-       this.set = [];
-       this.clear = [];
-};
-
-/* Static Members */
-
-es.TransactionProcessor.operationMap = {
-       // Retain
-       'retain': {
-               'commit': function( op ) {
-                       this.retain( op );
-               },
-               'rollback': function( op ) {
-                       this.retain( op );
-               }
-       },
-       // Insert
-       'insert': {
-               'commit': function( op ) {
-                       this.insert( op );
-               },
-               'rollback': function( op ) {
-                       this.remove( op );
-               }
-       },
-       // Remove
-       'remove': {
-               'commit': function( op ) {
-                       this.remove( op );
-               },
-               'rollback': function( op ) {
-                       this.insert( op );
-               }
-       },
-       // Change element attributes
-       'attribute': {
-               'commit': function( op ) {
-                       this.attribute( op, false );
-               },
-               'rollback': function( op ) {
-                       this.attribute( op, true );
-               }
-       },
-       // Change content annotations
-       'annotate': {
-               'commit': function( op ) {
-                       this.mark( op, false );
-               },
-               'rollback': function( op ) {
-                       this.mark( op, true );
-               }
-       }
-};
-
-/* Static Methods */
-
-es.TransactionProcessor.commit = function( doc, transaction ) {
-       var tp = new es.TransactionProcessor( doc, transaction );
-       tp.process( 'commit' );
-};
-
-es.TransactionProcessor.rollback = function( doc, transaction ) {
-       var tp = new es.TransactionProcessor( doc, transaction );
-       tp.process( 'rollback' );
-};
-
-/* Methods */
-
-es.TransactionProcessor.prototype.process = function( method ) {
-       var operations = this.transaction.getOperations();
-       for ( var i = 0, length = operations.length; i < length; i++ ) {
-               var operation = operations[i];
-               if ( operation.type in es.TransactionProcessor.operationMap ) {
-                       
es.TransactionProcessor.operationMap[operation.type][method].call( this, 
operation );
-               } else {
-                       throw 'Invalid operation error. Operation type is not 
supported: ' + operation.type;
-               }
-       }
-};
-
-es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes, 
parent, index ) {
-       var remove = 0;
-       if ( oldNodes ) {
-               if ( oldNodes[0] === oldNodes[0].getRoot() ) {
-                       parent = oldNodes[0];
-                       index = 0;
-                       remove = parent.getChildren().length;
-               } else {
-                       parent = oldNodes[0].getParent();
-                       index = parent.indexOf( oldNodes[0] );
-                       remove = oldNodes.length;
-               }
-       }
-       // Try to perform this in a single operation if possible, this reduces 
the number of UI updates
-       // TODO: Introduce a global for max argument length - 1024 is also 
assumed in es.insertIntoArray
-       var newNodes = es.DocumentModel.createNodesFromData( newData );
-       if ( newNodes.length < 1024 ) {
-               parent.splice.apply( parent, [index, remove].concat( newNodes ) 
);
-       } else {
-               parent.splice.apply( parent, [index, remove] );
-               // Safe to call with arbitrary length of newNodes
-               es.insertIntoArray( parent, index, newNodes );
-       }
-};
-
-/**
- * Get the parent node that would be affected by inserting given data into 
it's child.
- * 
- * This is used when inserting data that closes and reopens one or more parent 
nodes into a child
- * node, which requires rebuilding at a higher level.
- * 
- * @method
- * @param {es.DocumentNode} node Child node to start from
- * @param {Array} data Data to inspect for closings
- * @returns {es.DocumentNode} Lowest level parent node being affected
- */
-es.TransactionProcessor.prototype.getScope = function( node, data ) {
-       var i,
-               length,
-               level = 0,
-               max = 0;
-       for ( i = 0, length = data.length; i < length; i++ ) {
-               if ( typeof data[i].type === 'string' ) {
-                       level += data[i].type.charAt( 0 ) === '/' ? 1 : -1;
-                       max = Math.max( max, level );
-               }
-       }
-       if ( max > 0 ) {
-               for ( i = 0; i < max - 1; i++ ) {
-                       node = node.getParent();
-               }
-       }
-       return node;
-};
-
-es.TransactionProcessor.prototype.applyAnnotations = function( to ) {
-       var i,
-               j,
-               length,
-               annotation;
-       // Handle annotations
-       if ( this.set.length ) {
-               for ( i = 0, length = this.set.length; i < length; i++ ) {
-                       annotation = this.set[i];
-                       // Auto-build annotation hash
-                       if ( annotation.hash === undefined ) {
-                               annotation.hash = 
es.DocumentModel.getAnnotationHash( annotation );
-                       }
-                       for ( j = this.cursor; j < to; j++ ) {
-                               // Auto-convert to array
-                               if ( es.isArray( this.model.data[j] ) ) {
-                                       this.model.data[j].push( annotation );
-                               } else {
-                                       this.model.data[j] = 
[this.model.data[j], annotation];
-                               }
-                       }
-               }
-       }
-       if ( this.clear.length ) {
-               for ( i = 0, length = this.clear.length; i < length; i++ ) {
-                       annotation = this.clear[i];
-                       // Auto-build annotation hash
-                       if ( annotation.hash === undefined ) {
-                               annotation.hash = 
es.DocumentModel.getAnnotationHash( annotation );
-                       }
-                       for ( j = this.cursor; j < to; j++ ) {
-                               var index = 
es.DocumentModel.getIndexOfAnnotation( this.model.data[j], annotation );
-                               if ( index !== -1 ) {
-                                       this.model.data[j].splice( index, 1 );
-                               }
-                               // Auto-convert to string
-                               if ( this.model.data[j].length === 1 ) {
-                                       this.model.data[j] = 
this.model.data[j][0];
-                               }
-                       }
-               }
-       }
-};
-
-es.TransactionProcessor.prototype.retain = function( op ) {
-       this.applyAnnotations( this.cursor + op.length );
-       this.cursor += op.length;
-};
-
-es.TransactionProcessor.prototype.insert = function( op ) {
-       var node,
-               index,
-               offset;
-       if ( es.DocumentModel.isStructuralOffset( this.model.data, this.cursor 
) ) {
-               es.insertIntoArray( this.model.data, this.cursor, op.data );
-               this.applyAnnotations( this.cursor + op.data.length );
-               node = this.model.getNodeFromOffset( this.cursor );
-               offset = this.model.getOffsetFromNode( node );
-               index = node.getIndexFromOffset( this.cursor - offset );
-               this.rebuildNodes( op.data, null, node, index );
-       } else {
-               node = this.model.getNodeFromOffset( this.cursor );
-               if ( node.getParent() === this.model ) {
-                       offset = this.model.getOffsetFromNode( node );
-                       index = this.model.getIndexFromOffset( this.cursor - 
offset );
-               } else {
-                       node = this.getScope( node, op.data );
-                       offset = this.model.getOffsetFromNode( node );
-                       index = node.getIndexFromOffset( this.cursor - offset );
-               }
-               if ( es.DocumentModel.containsElementData( op.data ) ) {
-                       // Perform insert on linear data model
-                       es.insertIntoArray( this.model.data, this.cursor, 
op.data );
-                       this.applyAnnotations( this.cursor + op.data.length );
-                       // Synchronize model tree
-                       if ( offset === -1 ) {
-                               throw 'Invalid offset error. Node is not in 
model tree';
-                       }
-                       this.rebuildNodes(
-                               this.model.data.slice( offset, offset + 
node.getElementLength() + op.data.length ),
-                               [node]
-                       );
-               } else {
-                       // Perform insert on linear data model
-                       // TODO this is duplicated from above
-                       es.insertIntoArray( this.model.data, this.cursor, 
op.data );
-                       this.applyAnnotations( this.cursor + op.data.length );
-                       // Update model tree
-                       node.adjustContentLength( op.data.length, true );
-                       node.emit( 'update', this.cursor - offset );
-               }
-       }
-       this.cursor += op.data.length;
-};
-
-es.TransactionProcessor.prototype.remove = function( op ) {
-       if ( es.DocumentModel.containsElementData( op.data ) ) {
-               // Figure out which nodes are covered by the removal
-               var ranges = this.model.selectNodes( new es.Range( this.cursor, 
this.cursor + op.data.length ) );
-               var oldNodes = [], newData = [], firstKeptNode = true, 
lastElement;
-               for ( var i = 0; i < ranges.length; i++ ) {
-                       oldNodes.push( ranges[i].node );
-                       if ( ranges[i].range !== undefined ) {
-                               // We have to keep part of this node
-                               if ( firstKeptNode ) {
-                                       // This is the first node we're keeping
-                                       // Keep its opening as well
-                                       newData.push( 
ranges[i].node.getElement() );
-                                       firstKeptNode = false;
-                               }
-                               // Compute the start and end offset of this node
-                               // We could do that with getOffsetFromNode() but
-                               // we already have all the numbers we need so 
why would we
-                               var     startOffset = 
ranges[i].globalRange.start - ranges[i].range.start,
-                                       endOffset = startOffset + 
ranges[i].node.getContentLength(),
-                                       // Get this node's data
-                                       nodeData = this.model.data.slice( 
startOffset, endOffset );
-                               // Remove data covered by the range from 
nodeData
-                               nodeData.splice( ranges[i].range.start, 
ranges[i].range.end - ranges[i].range.start );
-                               // What remains in nodeData is the data we need 
to keep
-                               // Append it to newData
-                               newData = newData.concat( nodeData );
-                               
-                               lastElement = ranges[i].node.getElementType();
-                       }
-               }
-               if ( lastElement !== undefined ) {
-                       // Keep the closing of the last element that was 
partially kept
-                       newData.push( { 'type': '/' + lastElement } );
-               }
-               // Update the linear model
-               this.model.data.splice( this.cursor, op.data.length );
-               // Perform the rebuild. This updates the model tree
-               this.rebuildNodes( newData, oldNodes );
-       } else {
-               // We're removing content only. Take a shortcut
-               // Get the node we are removing content from
-               var node = this.model.getNodeFromOffset( this.cursor );
-               // Update model tree
-               node.adjustContentLength( -op.data.length, true );
-               // Update the linear model
-               this.model.data.splice( this.cursor, op.data.length );
-               // Emit an update so things sync up
-               var offset = this.model.getOffsetFromNode( node );
-               node.emit( 'update', this.cursor - offset );
-       }
-};
-
-es.TransactionProcessor.prototype.attribute = function( op, invert ) {
-       var element = this.model.data[this.cursor];
-       if ( element.type === undefined ) {
-               throw 'Invalid element error. Can not set attributes on 
non-element data.';
-       }
-       if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && 
invert ) ) {
-               // Automatically initialize attributes object
-               if ( !element.attributes ) {
-                       element.attributes = {};
-               }
-               element.attributes[op.key] = op.value;
-       } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 
'set' && invert ) ) {
-               if ( element.attributes ) {
-                       delete element.attributes[op.key];
-               }
-               // Automatically clean up attributes object
-               var empty = true;
-               for ( var key in element.attributes ) {
-                       empty = false;
-                       break;
-               }
-               if ( empty ) {
-                       delete element.attributes;
-               }
-       } else {
-               throw 'Invalid method error. Can not operate attributes this 
way: ' + method;
-       }
-};
-
-es.TransactionProcessor.prototype.mark = function( op, invert ) {
-       var target;
-       if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && 
invert ) ) {
-               target = this.set;
-       } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 
'set' && invert ) ) {
-               target = this.clear;
-       } else {
-               throw 'Invalid method error. Can not operate attributes this 
way: ' + method;
-       }
-       if ( op.bias === 'start' ) {
-               target.push( op.annotation );
-       } else if ( op.bias === 'stop' ) {
-               var index = es.DocumentModel.getIndexOfAnnotation( target, 
op.annotation );
-               if ( index === -1 ) {
-                       throw 'Annotation stack error. Annotation is missing.';
-               }
-               target.splice( index, 1 );
-       }
-};

Added: trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
===================================================================
--- trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js         
                (rev 0)
+++ trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js 
2011-11-17 22:42:18 UTC (rev 103516)
@@ -0,0 +1,341 @@
+/**
+ * Creates an es.TransactionProcessor object.
+ * 
+ * @class
+ * @constructor
+ */
+es.TransactionProcessor = function( model, transaction ) {
+       this.model = model;
+       this.transaction = transaction;
+       this.cursor = 0;
+       this.set = [];
+       this.clear = [];
+};
+
+/* Static Members */
+
+es.TransactionProcessor.operationMap = {
+       // Retain
+       'retain': {
+               'commit': function( op ) {
+                       this.retain( op );
+               },
+               'rollback': function( op ) {
+                       this.retain( op );
+               }
+       },
+       // Insert
+       'insert': {
+               'commit': function( op ) {
+                       this.insert( op );
+               },
+               'rollback': function( op ) {
+                       this.remove( op );
+               }
+       },
+       // Remove
+       'remove': {
+               'commit': function( op ) {
+                       this.remove( op );
+               },
+               'rollback': function( op ) {
+                       this.insert( op );
+               }
+       },
+       // Change element attributes
+       'attribute': {
+               'commit': function( op ) {
+                       this.attribute( op, false );
+               },
+               'rollback': function( op ) {
+                       this.attribute( op, true );
+               }
+       },
+       // Change content annotations
+       'annotate': {
+               'commit': function( op ) {
+                       this.mark( op, false );
+               },
+               'rollback': function( op ) {
+                       this.mark( op, true );
+               }
+       }
+};
+
+/* Static Methods */
+
+es.TransactionProcessor.commit = function( doc, transaction ) {
+       var tp = new es.TransactionProcessor( doc, transaction );
+       tp.process( 'commit' );
+};
+
+es.TransactionProcessor.rollback = function( doc, transaction ) {
+       var tp = new es.TransactionProcessor( doc, transaction );
+       tp.process( 'rollback' );
+};
+
+/* Methods */
+
+es.TransactionProcessor.prototype.process = function( method ) {
+       var operations = this.transaction.getOperations();
+       for ( var i = 0, length = operations.length; i < length; i++ ) {
+               var operation = operations[i];
+               if ( operation.type in es.TransactionProcessor.operationMap ) {
+                       
es.TransactionProcessor.operationMap[operation.type][method].call( this, 
operation );
+               } else {
+                       throw 'Invalid operation error. Operation type is not 
supported: ' + operation.type;
+               }
+       }
+};
+
+es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes, 
parent, index ) {
+       var remove = 0;
+       if ( oldNodes ) {
+               if ( oldNodes[0] === oldNodes[0].getRoot() ) {
+                       parent = oldNodes[0];
+                       index = 0;
+                       remove = parent.getChildren().length;
+               } else {
+                       parent = oldNodes[0].getParent();
+                       index = parent.indexOf( oldNodes[0] );
+                       remove = oldNodes.length;
+               }
+       }
+       // Try to perform this in a single operation if possible, this reduces 
the number of UI updates
+       // TODO: Introduce a global for max argument length - 1024 is also 
assumed in es.insertIntoArray
+       var newNodes = es.DocumentModel.createNodesFromData( newData );
+       if ( newNodes.length < 1024 ) {
+               parent.splice.apply( parent, [index, remove].concat( newNodes ) 
);
+       } else {
+               parent.splice.apply( parent, [index, remove] );
+               // Safe to call with arbitrary length of newNodes
+               es.insertIntoArray( parent, index, newNodes );
+       }
+};
+
+/**
+ * Get the parent node that would be affected by inserting given data into 
it's child.
+ * 
+ * This is used when inserting data that closes and reopens one or more parent 
nodes into a child
+ * node, which requires rebuilding at a higher level.
+ * 
+ * @method
+ * @param {es.DocumentNode} node Child node to start from
+ * @param {Array} data Data to inspect for closings
+ * @returns {es.DocumentNode} Lowest level parent node being affected
+ */
+es.TransactionProcessor.prototype.getScope = function( node, data ) {
+       var i,
+               length,
+               level = 0,
+               max = 0;
+       for ( i = 0, length = data.length; i < length; i++ ) {
+               if ( typeof data[i].type === 'string' ) {
+                       level += data[i].type.charAt( 0 ) === '/' ? 1 : -1;
+                       max = Math.max( max, level );
+               }
+       }
+       if ( max > 0 ) {
+               for ( i = 0; i < max - 1; i++ ) {
+                       node = node.getParent();
+               }
+       }
+       return node;
+};
+
+es.TransactionProcessor.prototype.applyAnnotations = function( to ) {
+       var i,
+               j,
+               length,
+               annotation;
+       // Handle annotations
+       if ( this.set.length ) {
+               for ( i = 0, length = this.set.length; i < length; i++ ) {
+                       annotation = this.set[i];
+                       // Auto-build annotation hash
+                       if ( annotation.hash === undefined ) {
+                               annotation.hash = 
es.DocumentModel.getAnnotationHash( annotation );
+                       }
+                       for ( j = this.cursor; j < to; j++ ) {
+                               // Auto-convert to array
+                               if ( es.isArray( this.model.data[j] ) ) {
+                                       this.model.data[j].push( annotation );
+                               } else {
+                                       this.model.data[j] = 
[this.model.data[j], annotation];
+                               }
+                       }
+               }
+       }
+       if ( this.clear.length ) {
+               for ( i = 0, length = this.clear.length; i < length; i++ ) {
+                       annotation = this.clear[i];
+                       // Auto-build annotation hash
+                       if ( annotation.hash === undefined ) {
+                               annotation.hash = 
es.DocumentModel.getAnnotationHash( annotation );
+                       }
+                       for ( j = this.cursor; j < to; j++ ) {
+                               var index = 
es.DocumentModel.getIndexOfAnnotation( this.model.data[j], annotation );
+                               if ( index !== -1 ) {
+                                       this.model.data[j].splice( index, 1 );
+                               }
+                               // Auto-convert to string
+                               if ( this.model.data[j].length === 1 ) {
+                                       this.model.data[j] = 
this.model.data[j][0];
+                               }
+                       }
+               }
+       }
+};
+
+es.TransactionProcessor.prototype.retain = function( op ) {
+       this.applyAnnotations( this.cursor + op.length );
+       this.cursor += op.length;
+};
+
+es.TransactionProcessor.prototype.insert = function( op ) {
+       var node,
+               index,
+               offset;
+       if ( es.DocumentModel.isStructuralOffset( this.model.data, this.cursor 
) ) {
+               es.insertIntoArray( this.model.data, this.cursor, op.data );
+               this.applyAnnotations( this.cursor + op.data.length );
+               node = this.model.getNodeFromOffset( this.cursor );
+               offset = this.model.getOffsetFromNode( node );
+               index = node.getIndexFromOffset( this.cursor - offset );
+               this.rebuildNodes( op.data, null, node, index );
+       } else {
+               node = this.model.getNodeFromOffset( this.cursor );
+               if ( node.getParent() === this.model ) {
+                       offset = this.model.getOffsetFromNode( node );
+                       index = this.model.getIndexFromOffset( this.cursor - 
offset );
+               } else {
+                       node = this.getScope( node, op.data );
+                       offset = this.model.getOffsetFromNode( node );
+                       index = node.getIndexFromOffset( this.cursor - offset );
+               }
+               if ( es.DocumentModel.containsElementData( op.data ) ) {
+                       // Perform insert on linear data model
+                       es.insertIntoArray( this.model.data, this.cursor, 
op.data );
+                       this.applyAnnotations( this.cursor + op.data.length );
+                       // Synchronize model tree
+                       if ( offset === -1 ) {
+                               throw 'Invalid offset error. Node is not in 
model tree';
+                       }
+                       this.rebuildNodes(
+                               this.model.data.slice( offset, offset + 
node.getElementLength() + op.data.length ),
+                               [node]
+                       );
+               } else {
+                       // Perform insert on linear data model
+                       // TODO this is duplicated from above
+                       es.insertIntoArray( this.model.data, this.cursor, 
op.data );
+                       this.applyAnnotations( this.cursor + op.data.length );
+                       // Update model tree
+                       node.adjustContentLength( op.data.length, true );
+                       node.emit( 'update', this.cursor - offset );
+               }
+       }
+       this.cursor += op.data.length;
+};
+
+es.TransactionProcessor.prototype.remove = function( op ) {
+       if ( es.DocumentModel.containsElementData( op.data ) ) {
+               // Figure out which nodes are covered by the removal
+               var ranges = this.model.selectNodes( new es.Range( this.cursor, 
this.cursor + op.data.length ) );
+               var oldNodes = [], newData = [], firstKeptNode = true, 
lastElement;
+               for ( var i = 0; i < ranges.length; i++ ) {
+                       oldNodes.push( ranges[i].node );
+                       if ( ranges[i].range !== undefined ) {
+                               // We have to keep part of this node
+                               if ( firstKeptNode ) {
+                                       // This is the first node we're keeping
+                                       // Keep its opening as well
+                                       newData.push( 
ranges[i].node.getElement() );
+                                       firstKeptNode = false;
+                               }
+                               // Compute the start and end offset of this node
+                               // We could do that with getOffsetFromNode() but
+                               // we already have all the numbers we need so 
why would we
+                               var     startOffset = 
ranges[i].globalRange.start - ranges[i].range.start,
+                                       endOffset = startOffset + 
ranges[i].node.getContentLength(),
+                                       // Get this node's data
+                                       nodeData = this.model.data.slice( 
startOffset, endOffset );
+                               // Remove data covered by the range from 
nodeData
+                               nodeData.splice( ranges[i].range.start, 
ranges[i].range.end - ranges[i].range.start );
+                               // What remains in nodeData is the data we need 
to keep
+                               // Append it to newData
+                               newData = newData.concat( nodeData );
+                               
+                               lastElement = ranges[i].node.getElementType();
+                       }
+               }
+               if ( lastElement !== undefined ) {
+                       // Keep the closing of the last element that was 
partially kept
+                       newData.push( { 'type': '/' + lastElement } );
+               }
+               // Update the linear model
+               this.model.data.splice( this.cursor, op.data.length );
+               // Perform the rebuild. This updates the model tree
+               this.rebuildNodes( newData, oldNodes );
+       } else {
+               // We're removing content only. Take a shortcut
+               // Get the node we are removing content from
+               var node = this.model.getNodeFromOffset( this.cursor );
+               // Update model tree
+               node.adjustContentLength( -op.data.length, true );
+               // Update the linear model
+               this.model.data.splice( this.cursor, op.data.length );
+               // Emit an update so things sync up
+               var offset = this.model.getOffsetFromNode( node );
+               node.emit( 'update', this.cursor - offset );
+       }
+};
+
+es.TransactionProcessor.prototype.attribute = function( op, invert ) {
+       var element = this.model.data[this.cursor];
+       if ( element.type === undefined ) {
+               throw 'Invalid element error. Can not set attributes on 
non-element data.';
+       }
+       if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && 
invert ) ) {
+               // Automatically initialize attributes object
+               if ( !element.attributes ) {
+                       element.attributes = {};
+               }
+               element.attributes[op.key] = op.value;
+       } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 
'set' && invert ) ) {
+               if ( element.attributes ) {
+                       delete element.attributes[op.key];
+               }
+               // Automatically clean up attributes object
+               var empty = true;
+               for ( var key in element.attributes ) {
+                       empty = false;
+                       break;
+               }
+               if ( empty ) {
+                       delete element.attributes;
+               }
+       } else {
+               throw 'Invalid method error. Can not operate attributes this 
way: ' + method;
+       }
+};
+
+es.TransactionProcessor.prototype.mark = function( op, invert ) {
+       var target;
+       if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && 
invert ) ) {
+               target = this.set;
+       } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 
'set' && invert ) ) {
+               target = this.clear;
+       } else {
+               throw 'Invalid method error. Can not operate attributes this 
way: ' + method;
+       }
+       if ( op.bias === 'start' ) {
+               target.push( op.annotation );
+       } else if ( op.bias === 'stop' ) {
+               var index = es.DocumentModel.getIndexOfAnnotation( target, 
op.annotation );
+               if ( index === -1 ) {
+                       throw 'Annotation stack error. Annotation is missing.';
+               }
+               target.splice( index, 1 );
+       }
+};

Modified: trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js
===================================================================
--- trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js 
2011-11-17 22:16:22 UTC (rev 103515)
+++ trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js 
2011-11-17 22:42:18 UTC (rev 103516)
@@ -665,7 +665,7 @@
  * @method
  * @param {Integer} offset
  * @param {Array} data
- * @returns {es.Transaction}
+ * @returns {es.TransactionModel}
  */
 es.DocumentModel.prototype.prepareInsertion = function( offset, data ) {
        /**
@@ -719,7 +719,7 @@
                return workingData || data;
        }
        
-       var tx = new es.Transaction(),
+       var tx = new es.TransactionModel(),
                insertedData = data, // may be cloned and modified
                isStructuralLoc,
                wrappingElementType;
@@ -823,7 +823,7 @@
  * 
  * @method
  * @param {es.Range} range
- * @returns {es.Transaction}
+ * @returns {es.TransactionModel}
  */
 
 es.DocumentModel.prototype.prepareRemoval = function( range ) {
@@ -857,7 +857,7 @@
                return true;
        }
        
-       var tx = new es.Transaction(), selectedNodes, selectedNode, startNode, 
endNode, i;
+       var tx = new es.TransactionModel(), selectedNodes, selectedNode, 
startNode, endNode, i;
        range.normalize();
        if ( range.start === range.end ) {
                // Empty range, nothing to do
@@ -920,10 +920,10 @@
  * Generates a transaction which annotates content within a given range.
  * 
  * @method
- * @returns {es.Transaction}
+ * @returns {es.TransactionModel}
  */
 es.DocumentModel.prototype.prepareContentAnnotation = function( range, method, 
annotation ) {
-       var tx = new es.Transaction();
+       var tx = new es.TransactionModel();
        range.normalize();
        if ( annotation.hash === undefined ) {
                annotation.hash = es.DocumentModel.getAnnotationHash( 
annotation );
@@ -983,10 +983,10 @@
  * Generates a transaction which changes attributes on an element at a given 
offset.
  * 
  * @method
- * @returns {es.Transaction}
+ * @returns {es.TransactionModel}
  */
 es.DocumentModel.prototype.prepareElementAttributeChange = function( offset, 
method, key, value ) {
-       var tx = new es.Transaction();
+       var tx = new es.TransactionModel();
        if ( offset ) {
                tx.pushRetain( offset );
        }
@@ -1008,7 +1008,7 @@
  * Applies a transaction to the content data.
  * 
  * @method
- * @param {es.Transaction}
+ * @param {es.TransactionModel}
  */
 es.DocumentModel.prototype.commit = function( transaction ) {
        es.TransactionProcessor.commit( this, transaction );
@@ -1018,7 +1018,7 @@
  * Reverses a transaction's effects on the content data.
  * 
  * @method
- * @param {es.Transaction}
+ * @param {es.TransactionModel}
  */
 es.DocumentModel.prototype.rollback = function( transaction ) {
        es.TransactionProcessor.rollback( this, transaction );

Copied: trunk/extensions/VisualEditor/modules/es/models/es.TransactionModel.js 
(from rev 103498, trunk/extensions/VisualEditor/modules/es/es.Transaction.js)
===================================================================
--- trunk/extensions/VisualEditor/modules/es/models/es.TransactionModel.js      
                        (rev 0)
+++ trunk/extensions/VisualEditor/modules/es/models/es.TransactionModel.js      
2011-11-17 22:42:18 UTC (rev 103516)
@@ -0,0 +1,150 @@
+/**
+ * Creates an es.TransactionModel object.
+ * 
+ * @class
+ * @constructor
+ * @param {Object[]} operations List of operations
+ */
+es.TransactionModel = function( operations ) {
+       this.operations = es.isArray( operations ) ? operations : [];
+       this.lengthDiff = 0;
+};
+
+/* Methods */
+
+/**
+ * Gets a list of all operations.
+ * 
+ * @method
+ * @returns {Object[]} List of operations
+ */
+es.TransactionModel.prototype.getOperations = function() {
+       return this.operations;
+};
+
+/**
+ * Gets the difference in content length this transaction will cause if 
applied.
+ * 
+ * @method
+ * @returns {Integer} Difference in content length
+ */
+es.TransactionModel.prototype.getLengthDiff = function() {
+       return this.lengthDiff;
+};
+
+/**
+ * Merges consecutive operations of the same type.
+ * 
+ * @method
+ */
+es.TransactionModel.prototype.optimize = function() {
+       for ( var i = 0; i < this.operations.length - 1; i++ ) {
+               var a = this.operations[i];
+               var b = this.operations[i + 1];
+               if ( a.type === b.type ) {
+                       switch ( a.type ) {
+                               case 'retain':
+                                       a.length += b.length;
+                                       this.operations.splice( i + 1, 1 );
+                                       i--;
+                                       break;
+                               case 'insert':
+                               case 'remove':
+                                       a.data = a.data.concat( b.data );
+                                       this.operations.splice( i + 1, 1 );
+                                       i--;
+                                       break;
+                       }
+               }
+       }
+};
+
+/**
+ * Adds a retain operation.
+ * 
+ * @method
+ * @param {Integer} length Length of content data to retain
+ */
+es.TransactionModel.prototype.pushRetain = function( length ) {
+       this.operations.push( {
+               'type': 'retain',
+               'length': length
+       } );
+};
+
+/**
+ * Adds an insertion operation.
+ * 
+ * @method
+ * @param {Array} data Data to retain
+ */
+es.TransactionModel.prototype.pushInsert = function( data ) {
+       this.operations.push( {
+               'type': 'insert',
+               'data': data
+       } );
+       this.lengthDiff += data.length;
+};
+
+/**
+ * Adds a removal operation.
+ * 
+ * @method
+ * @param {Array} data Data to remove
+ */
+es.TransactionModel.prototype.pushRemove = function( data ) {
+       this.operations.push( {
+               'type': 'remove',
+               'data': data
+       } );
+       this.lengthDiff -= data.length;
+};
+
+/**
+ * Adds an element attribute change operation.
+ * 
+ * @method
+ * @param {String} method Method to use, either "set" or "clear"
+ * @param {String} key Name of attribute to change
+ * @param {Mixed} value Value to set attribute to, or value of attribute being 
cleared
+ */
+es.TransactionModel.prototype.pushChangeElementAttribute = function( method, 
key, value ) {
+       this.operations.push( {
+               'type': 'attribute',
+               'method': method,
+               'key': key,
+               'value': value
+       } );
+};
+
+/**
+ * Adds a start annotating operation.
+ * 
+ * @method
+ * @param {String} method Method to use, either "set" or "clear"
+ * @param {Object} annotation Annotation object to start setting or clearing 
from content data
+ */
+es.TransactionModel.prototype.pushStartAnnotating = function( method, 
annotation ) {
+       this.operations.push( {
+               'type': 'annotate',
+               'method': method,
+               'bias': 'start',
+               'annotation': annotation
+       } );
+};
+
+/**
+ * Adds a stop annotating operation.
+ * 
+ * @method
+ * @param {String} method Method to use, either "set" or "clear"
+ * @param {Object} annotation Annotation object to stop setting or clearing 
from content data
+ */
+es.TransactionModel.prototype.pushStopAnnotating = function( method, 
annotation ) {
+       this.operations.push( {
+               'type': 'annotate',
+               'method': method,
+               'bias': 'stop',
+               'annotation': annotation
+       } );
+};

Deleted: trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js
===================================================================
--- trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js      
2011-11-17 22:16:22 UTC (rev 103515)
+++ trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js      
2011-11-17 22:42:18 UTC (rev 103516)
@@ -1,246 +0,0 @@
-module( 'es' );
-
-test( 'es.TransactionProcessor', 18, function() {
-       var documentModel = es.DocumentModel.newFromPlainObject( esTest.obj );
-
-       // FIXME: These tests shouldn't use prepareFoo() because those functions
-       // normalize the transactions they create and are tested separately.
-       // We should be creating transactions directly and feeding those into
-       // commit()/rollback() --Roan
-       var elementAttributeChange = 
documentModel.prepareElementAttributeChange(
-               0, 'set', 'test', 1
-       );
-
-       // Test 1
-       es.TransactionProcessor.commit( documentModel, elementAttributeChange );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph', 'attributes': { 'test': 1 } },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'commit applies an element attribute change transaction to the 
content'
-       );
-
-       // Test 2
-       es.TransactionProcessor.rollback( documentModel, elementAttributeChange 
);
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'rollback reverses the effect of an element attribute change 
transaction on the content'
-       );
-
-       var contentAnnotation = documentModel.prepareContentAnnotation(
-               new es.Range( 1, 4 ), 'set', { 'type': 'textStyle/bold' }
-       );
-
-       // Test 3
-       es.TransactionProcessor.commit( documentModel, contentAnnotation );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       ['a', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       [
-                               'c',
-                               { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' },
-                               { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }
-                       ],
-                       { 'type': '/paragraph' }
-               ],
-               'commit applies a content annotation transaction to the content'
-       );
-
-       // Test 4
-       es.TransactionProcessor.rollback( documentModel, contentAnnotation );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'rollback reverses the effect of a content annotation 
transaction on the content'
-       );
-
-       var insertion = documentModel.prepareInsertion( 3, ['d'] );
-
-       // Test 5
-       es.TransactionProcessor.commit( documentModel, insertion );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 6 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       'd',
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'commit applies an insertion transaction to the content'
-       );
-
-       // Test 6
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               [
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       'd',
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
-               ],
-               'commit keeps model tree up to date with insertions'
-       );
-
-       // Test 7
-       es.TransactionProcessor.rollback( documentModel, insertion );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'rollback reverses the effect of an insertion transaction on 
the content'
-       );
-
-       // Test 8
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               [
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
-               ],
-               'rollback keeps model tree up to date with insertions'
-       );
-
-       var removal = documentModel.prepareRemoval( new es.Range( 2, 4 ) );
-
-       // Test 9
-       es.TransactionProcessor.commit( documentModel, removal );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 3 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       { 'type': '/paragraph' }
-               ],
-               'commit applies a removal transaction to the content'
-       );
-
-       // Test 10
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               ['a'],
-               'commit keeps model tree up to date with removals'
-       );
-
-       // Test 11
-       es.TransactionProcessor.rollback( documentModel, removal );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'rollback reverses the effect of a removal transaction on the 
content'
-       );
-
-       // Test 12
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               [
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
-               ],
-               'rollback keeps model tree up to date with removals'
-       );
-       
-       var paragraphBreak = documentModel.prepareInsertion(
-               2, [{ 'type': '/paragraph' }, { 'type': 'paragraph' }]
-       );
-       
-       // Test 13
-       es.TransactionProcessor.commit( documentModel, paragraphBreak );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 7 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       { 'type': '/paragraph' },
-                       { 'type': 'paragraph' },
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'commit applies an insertion transaction that splits the 
paragraph'
-       );
-       
-       // Test 14
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               ['a'],
-               'commit keeps model tree up to date with paragraph split 
(paragraph 1)'
-       );
-       
-       // Test 15
-       deepEqual(
-               documentModel.getChildren()[1].getContent(),
-               [
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
-               ],
-               'commit keeps model tree up to date with paragraph split 
(paragraph 2)'
-       );
-
-       // Test 16
-       es.TransactionProcessor.rollback( documentModel, paragraphBreak );
-       deepEqual(
-               documentModel.getData( new es.Range( 0, 5 ) ),
-               [
-                       { 'type': 'paragraph' },
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
-                       { 'type': '/paragraph' }
-               ],
-               'rollback reverses the effect of a paragraph split on the 
content'
-       );
-       
-       // Test 17
-       deepEqual(
-               documentModel.getChildren()[0].getContent(),
-               [
-                       'a',
-                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
-                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
-               ],
-               'rollback keeps model tree up to date with paragraph split 
(paragraphs are merged back)'
-       );
-       
-       // Test 18
-       deepEqual(
-               documentModel.getChildren()[1].getElementType(),
-               'table',
-               'rollback keeps model tree up to date with paragraph split 
(table follows the paragraph)'
-       );
-} );

Added: trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js
===================================================================
--- trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js      
                        (rev 0)
+++ trunk/extensions/VisualEditor/tests/es/es.TransactionProcessor.test.js      
2011-11-17 22:42:18 UTC (rev 103516)
@@ -0,0 +1,246 @@
+module( 'es' );
+
+test( 'es.TransactionProcessor', 18, function() {
+       var documentModel = es.DocumentModel.newFromPlainObject( esTest.obj );
+
+       // FIXME: These tests shouldn't use prepareFoo() because those functions
+       // normalize the transactions they create and are tested separately.
+       // We should be creating transactions directly and feeding those into
+       // commit()/rollback() --Roan
+       var elementAttributeChange = 
documentModel.prepareElementAttributeChange(
+               0, 'set', 'test', 1
+       );
+
+       // Test 1
+       es.TransactionProcessor.commit( documentModel, elementAttributeChange );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph', 'attributes': { 'test': 1 } },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'commit applies an element attribute change transaction to the 
content'
+       );
+
+       // Test 2
+       es.TransactionProcessor.rollback( documentModel, elementAttributeChange 
);
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'rollback reverses the effect of an element attribute change 
transaction on the content'
+       );
+
+       var contentAnnotation = documentModel.prepareContentAnnotation(
+               new es.Range( 1, 4 ), 'set', { 'type': 'textStyle/bold' }
+       );
+
+       // Test 3
+       es.TransactionProcessor.commit( documentModel, contentAnnotation );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       ['a', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       [
+                               'c',
+                               { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' },
+                               { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }
+                       ],
+                       { 'type': '/paragraph' }
+               ],
+               'commit applies a content annotation transaction to the content'
+       );
+
+       // Test 4
+       es.TransactionProcessor.rollback( documentModel, contentAnnotation );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'rollback reverses the effect of a content annotation 
transaction on the content'
+       );
+
+       var insertion = documentModel.prepareInsertion( 3, ['d'] );
+
+       // Test 5
+       es.TransactionProcessor.commit( documentModel, insertion );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 6 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       'd',
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'commit applies an insertion transaction to the content'
+       );
+
+       // Test 6
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               [
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       'd',
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
+               ],
+               'commit keeps model tree up to date with insertions'
+       );
+
+       // Test 7
+       es.TransactionProcessor.rollback( documentModel, insertion );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'rollback reverses the effect of an insertion transaction on 
the content'
+       );
+
+       // Test 8
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               [
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
+               ],
+               'rollback keeps model tree up to date with insertions'
+       );
+
+       var removal = documentModel.prepareRemoval( new es.Range( 2, 4 ) );
+
+       // Test 9
+       es.TransactionProcessor.commit( documentModel, removal );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 3 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       { 'type': '/paragraph' }
+               ],
+               'commit applies a removal transaction to the content'
+       );
+
+       // Test 10
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               ['a'],
+               'commit keeps model tree up to date with removals'
+       );
+
+       // Test 11
+       es.TransactionProcessor.rollback( documentModel, removal );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'rollback reverses the effect of a removal transaction on the 
content'
+       );
+
+       // Test 12
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               [
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
+               ],
+               'rollback keeps model tree up to date with removals'
+       );
+       
+       var paragraphBreak = documentModel.prepareInsertion(
+               2, [{ 'type': '/paragraph' }, { 'type': 'paragraph' }]
+       );
+       
+       // Test 13
+       es.TransactionProcessor.commit( documentModel, paragraphBreak );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 7 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       { 'type': '/paragraph' },
+                       { 'type': 'paragraph' },
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'commit applies an insertion transaction that splits the 
paragraph'
+       );
+       
+       // Test 14
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               ['a'],
+               'commit keeps model tree up to date with paragraph split 
(paragraph 1)'
+       );
+       
+       // Test 15
+       deepEqual(
+               documentModel.getChildren()[1].getContent(),
+               [
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
+               ],
+               'commit keeps model tree up to date with paragraph split 
(paragraph 2)'
+       );
+
+       // Test 16
+       es.TransactionProcessor.rollback( documentModel, paragraphBreak );
+       deepEqual(
+               documentModel.getData( new es.Range( 0, 5 ) ),
+               [
+                       { 'type': 'paragraph' },
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }],
+                       { 'type': '/paragraph' }
+               ],
+               'rollback reverses the effect of a paragraph split on the 
content'
+       );
+       
+       // Test 17
+       deepEqual(
+               documentModel.getChildren()[0].getContent(),
+               [
+                       'a',
+                       ['b', { 'type': 'textStyle/bold', 'hash': 
'#textStyle/bold' }],
+                       ['c', { 'type': 'textStyle/italic', 'hash': 
'#textStyle/italic' }]
+               ],
+               'rollback keeps model tree up to date with paragraph split 
(paragraphs are merged back)'
+       );
+       
+       // Test 18
+       deepEqual(
+               documentModel.getChildren()[1].getElementType(),
+               'table',
+               'rollback keeps model tree up to date with paragraph split 
(table follows the paragraph)'
+       );
+} );

Modified: trunk/extensions/VisualEditor/tests/es/index.html
===================================================================
--- trunk/extensions/VisualEditor/tests/es/index.html   2011-11-17 22:16:22 UTC 
(rev 103515)
+++ trunk/extensions/VisualEditor/tests/es/index.html   2011-11-17 22:42:18 UTC 
(rev 103516)
@@ -18,7 +18,6 @@
                <script src="../../modules/qunit/qunit.js"></script>
                <script src="../../modules/es/es.js"></script>
                <script src="../../modules/es/es.Range.js"></script>
-               <script src="../../modules/es/es.Transaction.js"></script>
                <script 
src="../../modules/es/es.TransactionProcessor.js"></script>
 
                <!-- Bases -->
@@ -38,6 +37,7 @@
                <script 
src="../../modules/es/models/es.TableCellModel.js"></script>
                <script src="../../modules/es/models/es.TableModel.js"></script>
                <script 
src="../../modules/es/models/es.TableRowModel.js"></script>
+               <script 
src="../../modules/es/models/es.TransactionModel.js"></script>
 
                <!-- Tests -->
                <script src="es.testData.js"></script>


_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs

Reply via email to