2012-07-19 00:11:26 +00:00
|
|
|
/**
|
|
|
|
* VisualEditor data model TransactionProcessor class.
|
2012-07-19 21:25:16 +00:00
|
|
|
*
|
2012-07-19 00:11:26 +00:00
|
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
2011-11-14 23:04:36 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* NOTE: Instances of this class are not recyclable: you can only call .process() on them once.
|
|
|
|
*
|
2011-11-14 23:04:36 +00:00
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
*/
|
2012-09-06 23:15:55 +00:00
|
|
|
ve.dm.TransactionProcessor = function VeDmTransactionProcessor( doc, transaction, reversed ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
// Properties
|
|
|
|
this.document = doc;
|
2012-10-23 00:28:37 +00:00
|
|
|
this.transaction = transaction;
|
2012-06-20 01:20:28 +00:00
|
|
|
this.operations = transaction.getOperations();
|
|
|
|
this.synchronizer = new ve.dm.DocumentSynchronizer( doc );
|
|
|
|
this.reversed = reversed;
|
|
|
|
// Linear model offset that we're currently at. Operations in the transaction are ordered, so
|
|
|
|
// the cursor only ever moves forward.
|
2011-11-14 23:04:36 +00:00
|
|
|
this.cursor = 0;
|
2012-06-20 01:20:28 +00:00
|
|
|
// Adjustment used to convert between linear model offsets in the original linear model and
|
|
|
|
// in the half-updated linear model.
|
|
|
|
this.adjustment = 0;
|
2012-08-24 02:06:36 +00:00
|
|
|
// Set and clear are sets of annotations which should be added or removed to content being
|
|
|
|
// inserted or retained.
|
|
|
|
this.set = new ve.AnnotationSet();
|
|
|
|
this.clear = new ve.AnnotationSet();
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Static Members */
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Processing methods.
|
|
|
|
*
|
|
|
|
* Each method is specific to a type of action. Methods are called in the context of a transaction
|
|
|
|
* processor, so they work similar to normal methods on the object.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @member
|
|
|
|
*/
|
|
|
|
ve.dm.TransactionProcessor.processors = {};
|
2011-11-14 23:04:36 +00:00
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/* Static methods */
|
2011-11-14 23:04:36 +00:00
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.commit = function ( doc, transaction ) {
|
2012-10-23 00:28:37 +00:00
|
|
|
if ( transaction.hasBeenApplied() ) {
|
|
|
|
throw new Error( 'Cannot commit a transaction that has already been committed' );
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
new ve.dm.TransactionProcessor( doc, transaction, false ).process();
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.rollback = function ( doc, transaction ) {
|
2012-10-23 00:28:37 +00:00
|
|
|
if ( !transaction.hasBeenApplied() ) {
|
|
|
|
throw new Error( 'Cannot roll back a transaction that has not been committed' );
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
new ve.dm.TransactionProcessor( doc, transaction, true ).process();
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Execute a retain operation.
|
|
|
|
*
|
|
|
|
* This method is called within the context of a document synchronizer instance.
|
|
|
|
*
|
|
|
|
* This moves the cursor by op.length and applies annotations to the characters that the cursor
|
|
|
|
* moved over.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @method
|
|
|
|
* @param {Object} op Operation object:
|
Object management: Object create/inherit/clone utilities
* For the most common case:
- replace ve.extendClass with ve.inheritClass (chose slightly
different names to detect usage of the old/new one, and I
like 'inherit' better).
- move it up to below the constructor, see doc block for why.
* Cases where more than 2 arguments were passed to
ve.extendClass are handled differently depending on the case.
In case of a longer inheritance tree, the other arguments
could be omitted (like in "ve.ce.FooBar, ve.FooBar,
ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar,
because ve.ce.FooBar inherits from ve.Bar).
In the case of where it previously had two mixins with
ve.extendClass(), either one becomes inheritClass and one
a mixin, both to mixinClass().
No visible changes should come from this commit as the
instances still all have the same visible properties in the
end. No more or less than before.
* Misc.:
- Be consistent in calling parent constructors in the
same order as the inheritance.
- Add missing @extends and @param documentation.
- Replace invalid {Integer} type hint with {Number}.
- Consistent doc comments order:
@class, @abstract, @constructor, @extends, @params.
- Fix indentation errors
A fairly common mistake was a superfluous space before the
identifier on the assignment line directly below the
documentation comment.
$ ack "^ [^*]" --js modules/ve
- Typo "Inhertiance" -> "Inheritance".
- Replacing the other confusing comment "Inheritance" (inside
the constructor) with "Parent constructor".
- Add missing @abstract for ve.ui.Tool.
- Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js
- Add function names to all @constructor functions. Now that we
have inheritance it is important and useful to have these
functions not be anonymous.
Example of debug shot: http://cl.ly/image/1j3c160w3D45
Makes the difference between
< documentNode;
> ve_dm_DocumentNode
...
: ve_dm_BranchNode
...
: ve_dm_Node
...
: ve_dm_Node
...
: Object
...
without names (current situation):
< documentNode;
> Object
...
: Object
...
: Object
...
: Object
...
: Object
...
though before this commit, it really looks like this
(flattened since ve.extendClass really did a mixin):
< documentNode;
> Object
...
...
...
Pattern in Sublime (case-sensitive) to find nameless
constructor functions:
"^ve\..*\.([A-Z])([^\.]+) = function \("
Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
|
|
|
* @param {Number} op.length Number of elements to retain
|
2012-06-20 01:20:28 +00:00
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.processors.retain = function ( op ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
this.applyAnnotations( this.cursor + op.length );
|
|
|
|
this.cursor += op.length;
|
2012-03-14 21:02:34 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Execute an annotate operation.
|
|
|
|
*
|
|
|
|
* This method is called within the context of a document synchronizer instance.
|
|
|
|
*
|
|
|
|
* This will add an annotation to or remove an annotation from {this.set} or {this.clear}.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @method
|
|
|
|
* @param {Object} op Operation object
|
|
|
|
* @param {String} op.method Annotation method, either 'set' to add or 'clear' to remove
|
2012-08-24 02:06:36 +00:00
|
|
|
* @param {String} op.bias End point of marker, either 'start' to begin or 'stop' to end
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {String} op.annotation Annotation object to set or clear from content
|
|
|
|
* @throws 'Invalid annotation method'
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.processors.annotate = function ( op ) {
|
2012-08-24 02:06:36 +00:00
|
|
|
var target;
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( op.method === 'set' ) {
|
|
|
|
target = this.reversed ? this.clear : this.set;
|
|
|
|
} else if ( op.method === 'clear' ) {
|
|
|
|
target = this.reversed ? this.set : this.clear;
|
2012-03-14 21:02:34 +00:00
|
|
|
} else {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Invalid annotation method ' + op.method );
|
2012-03-14 21:02:34 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( op.bias === 'start' ) {
|
2012-08-24 02:06:36 +00:00
|
|
|
target.push( op.annotation );
|
2012-06-20 01:20:28 +00:00
|
|
|
} else {
|
2012-08-24 02:06:36 +00:00
|
|
|
target.remove( op.annotation );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Tree sync is done by applyAnnotations()
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2011-11-17 19:19:02 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Execute an attribute operation.
|
|
|
|
*
|
|
|
|
* This method is called within the context of a document synchronizer instance.
|
|
|
|
*
|
|
|
|
* This sets the attribute named op.key on the element at this.cursor to op.to , or unsets it if
|
|
|
|
* op.to === undefined . op.from is not checked against the old value, but is used instead of op.to
|
|
|
|
* in reverse mode. So if op.from is incorrect, the transaction will commit fine, but won't roll
|
|
|
|
* back correctly.
|
|
|
|
*
|
|
|
|
* @static
|
2011-11-17 19:19:02 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {Object} op Operation object
|
|
|
|
* @param {String} op.key: Attribute name
|
|
|
|
* @param {Mixed} op.from: Old attribute value, or undefined if not previously set
|
|
|
|
* @param {Mixed} op.to: New attribute value, or undefined to unset
|
2011-11-17 19:19:02 +00:00
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.processors.attribute = function ( op ) {
|
|
|
|
var element = this.document.data[this.cursor],
|
|
|
|
to = this.reversed ? op.from : op.to,
|
|
|
|
from = this.reversed ? op.to : op.from;
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( element.type === undefined ) {
|
2012-12-07 21:38:00 +00:00
|
|
|
throw new Error( 'Invalid element error, cannot set attributes on non-element data' );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( to === undefined ) {
|
|
|
|
// Clear
|
|
|
|
if ( element.attributes ) {
|
|
|
|
delete element.attributes[op.key];
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2011-12-06 22:04:18 +00:00
|
|
|
} else {
|
2012-06-20 01:20:28 +00:00
|
|
|
// Automatically initialize attributes object
|
|
|
|
if ( !element.attributes ) {
|
|
|
|
element.attributes = {};
|
2011-12-07 02:13:43 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Set
|
|
|
|
element.attributes[op.key] = to;
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
this.synchronizer.pushAttributeChange(
|
|
|
|
this.document.getNodeFromOffset( this.cursor + 1 ),
|
|
|
|
op.key,
|
|
|
|
from, to
|
|
|
|
);
|
2012-10-23 00:53:58 +00:00
|
|
|
this.setChangeMarker( this.cursor, 'attributes' );
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Execute a replace operation.
|
|
|
|
*
|
|
|
|
* This method is called within the context of a document synchronizer instance.
|
|
|
|
*
|
|
|
|
* This replaces a range of linear model data with another at this.cursor, figures out how the model
|
|
|
|
* tree needs to be synchronized, and queues this in the DocumentSynchronizer.
|
|
|
|
*
|
|
|
|
* op.remove isn't checked against the actual data (instead op.remove.length things are removed
|
|
|
|
* starting at this.cursor), but it's used instead of op.insert in reverse mode. So if
|
|
|
|
* op.remove is incorrect but of the right length, the transaction will commit fine, but won't roll
|
|
|
|
* back correctly.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @method
|
|
|
|
* @param {Object} op Operation object
|
|
|
|
* @param {Array} op.remove Linear model data to remove
|
|
|
|
* @param {Array} op.insert Linear model data to insert
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.processors.replace = function ( op ) {
|
2012-10-23 00:53:58 +00:00
|
|
|
var node, selection, range, parentOffset,
|
2012-08-02 18:46:13 +00:00
|
|
|
remove = this.reversed ? op.insert : op.remove,
|
2012-06-20 01:20:28 +00:00
|
|
|
insert = this.reversed ? op.remove : op.insert,
|
|
|
|
removeIsContent = ve.dm.Document.isContentData( remove ),
|
|
|
|
insertIsContent = ve.dm.Document.isContentData( insert ),
|
2012-08-02 18:46:13 +00:00
|
|
|
removeHasStructure = ve.dm.Document.containsElementData( remove ),
|
|
|
|
insertHasStructure = ve.dm.Document.containsElementData( insert ),
|
|
|
|
operation = op,
|
|
|
|
removeLevel = 0,
|
|
|
|
insertLevel = 0,
|
|
|
|
i,
|
|
|
|
type,
|
|
|
|
prevCursor,
|
|
|
|
affectedRanges = [],
|
|
|
|
scope,
|
|
|
|
minInsertLevel = 0,
|
|
|
|
coveringRange,
|
|
|
|
scopeStart,
|
|
|
|
scopeEnd,
|
|
|
|
opAdjustment = 0,
|
|
|
|
opRemove,
|
|
|
|
opInsert;
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( removeIsContent && insertIsContent ) {
|
2012-03-12 03:55:32 +00:00
|
|
|
// Content replacement
|
|
|
|
// Update the linear model
|
2012-10-30 01:42:12 +00:00
|
|
|
this.document.spliceData( this.cursor, remove.length, insert );
|
2012-06-20 01:20:28 +00:00
|
|
|
this.applyAnnotations( this.cursor + insert.length );
|
2012-03-12 03:55:32 +00:00
|
|
|
// Get the node containing the replaced content
|
2012-06-20 01:20:28 +00:00
|
|
|
selection = this.document.selectNodes(
|
|
|
|
new ve.Range(
|
|
|
|
this.cursor - this.adjustment,
|
|
|
|
this.cursor - this.adjustment + remove.length
|
|
|
|
),
|
|
|
|
'leaves'
|
|
|
|
);
|
2012-10-23 00:41:00 +00:00
|
|
|
node = selection[0].node;
|
|
|
|
if (
|
|
|
|
!removeHasStructure && !insertHasStructure &&
|
|
|
|
selection.length === 1 &&
|
|
|
|
node && node.getType() === 'text'
|
|
|
|
) {
|
|
|
|
// Text-only replacement
|
|
|
|
// Queue a resize for the text node
|
|
|
|
this.synchronizer.pushResize( node, insert.length - remove.length );
|
|
|
|
} else {
|
2012-10-12 18:04:15 +00:00
|
|
|
// Replacement is not exclusively text
|
2012-06-20 01:20:28 +00:00
|
|
|
// Rebuild all covered nodes
|
2012-08-02 18:46:13 +00:00
|
|
|
range = new ve.Range(
|
2012-11-21 20:02:32 +00:00
|
|
|
selection[0].nodeOuterRange.start,
|
|
|
|
selection[selection.length - 1].nodeOuterRange.end
|
2012-08-02 18:46:13 +00:00
|
|
|
);
|
2012-06-20 01:20:28 +00:00
|
|
|
this.synchronizer.pushRebuild( range,
|
|
|
|
new ve.Range( range.start + this.adjustment,
|
|
|
|
range.end + this.adjustment + insert.length - remove.length )
|
|
|
|
);
|
|
|
|
}
|
2012-10-23 00:53:58 +00:00
|
|
|
// Set change markers on the parents of the affected nodes
|
|
|
|
for ( i = 0; i < selection.length; i++ ) {
|
2012-11-19 22:19:13 +00:00
|
|
|
parentOffset = ( selection[i].parentOuterRange || selection[i].nodeOuterRange ).start;
|
|
|
|
this.setChangeMarker( parentOffset + this.adjustment, 'content' );
|
2012-10-23 00:53:58 +00:00
|
|
|
}
|
2012-03-12 03:55:32 +00:00
|
|
|
// Advance the cursor
|
2012-06-20 01:20:28 +00:00
|
|
|
this.cursor += insert.length;
|
|
|
|
this.adjustment += insert.length - remove.length;
|
2012-03-12 03:55:32 +00:00
|
|
|
} else {
|
|
|
|
// Structural replacement
|
|
|
|
// It's possible that multiple replace operations are needed before the
|
|
|
|
// model is back in a consistent state. This loop applies the current
|
|
|
|
// replace operation to the linear model, then keeps applying subsequent
|
|
|
|
// operations until the model is consistent. We keep track of the changes
|
|
|
|
// and queue a single rebuild after the loop finishes.
|
|
|
|
while ( true ) {
|
2012-07-19 03:40:49 +00:00
|
|
|
if ( operation.type === 'replace' ) {
|
2012-08-10 23:49:14 +00:00
|
|
|
opRemove = this.reversed ? operation.insert : operation.remove;
|
2012-08-02 18:46:13 +00:00
|
|
|
opInsert = this.reversed ? operation.remove : operation.insert;
|
2012-06-20 01:20:28 +00:00
|
|
|
// Update the linear model for this insert
|
2012-10-30 01:42:12 +00:00
|
|
|
this.document.spliceData( this.cursor, opRemove.length, opInsert );
|
2012-06-20 01:20:28 +00:00
|
|
|
affectedRanges.push( new ve.Range(
|
|
|
|
this.cursor - this.adjustment,
|
|
|
|
this.cursor - this.adjustment + opRemove.length
|
|
|
|
) );
|
|
|
|
prevCursor = this.cursor;
|
|
|
|
this.cursor += opInsert.length;
|
|
|
|
// Paint the removed selection, figure out which nodes were
|
|
|
|
// covered, and add their ranges to the affected ranges list
|
|
|
|
if ( opRemove.length > 0 ) {
|
|
|
|
selection = this.document.selectNodes( new ve.Range(
|
|
|
|
prevCursor - this.adjustment,
|
|
|
|
prevCursor + opRemove.length - this.adjustment
|
|
|
|
), 'siblings' );
|
|
|
|
for ( i = 0; i < selection.length; i++ ) {
|
2012-10-23 00:48:22 +00:00
|
|
|
affectedRanges.push( selection[i].nodeOuterRange );
|
2012-10-23 00:53:58 +00:00
|
|
|
if (
|
|
|
|
selection[i].nodeOuterRange.start < prevCursor - this.adjustment &&
|
|
|
|
selection[i].node.canContainContent()
|
|
|
|
) {
|
|
|
|
// The opening element survives, so this
|
|
|
|
// node will have some of its content
|
|
|
|
// removed and/or have another node merged
|
|
|
|
// into it. Mark the node.
|
|
|
|
// TODO detect special case where closing is replaced
|
|
|
|
parentOffset = selection[i].nodeOuterRange.start + this.adjustment;
|
|
|
|
this.setChangeMarker( parentOffset, 'content' );
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Walk through the remove and insert data
|
2012-03-12 03:55:32 +00:00
|
|
|
// and keep track of the element depth change (level)
|
|
|
|
// for each of these two separately. The model is
|
|
|
|
// only consistent if both levels are zero.
|
|
|
|
for ( i = 0; i < opRemove.length; i++ ) {
|
|
|
|
type = opRemove[i].type;
|
2012-07-19 03:40:49 +00:00
|
|
|
if ( type !== undefined ) {
|
|
|
|
if ( type.charAt( 0 ) === '/' ) {
|
|
|
|
// Closing element
|
|
|
|
removeLevel--;
|
|
|
|
} else {
|
|
|
|
// Opening element
|
|
|
|
removeLevel++;
|
|
|
|
}
|
2012-03-12 03:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Keep track of the scope of the insertion
|
|
|
|
// Normally this is the node we're inserting into, except if the
|
|
|
|
// insertion closes elements it doesn't open (i.e. splits elements),
|
|
|
|
// in which case it's the affected ancestor
|
|
|
|
for ( i = 0; i < opInsert.length; i++ ) {
|
|
|
|
type = opInsert[i].type;
|
2012-07-19 03:40:49 +00:00
|
|
|
if ( type !== undefined ) {
|
|
|
|
if ( type.charAt( 0 ) === '/' ) {
|
|
|
|
// Closing element
|
|
|
|
insertLevel--;
|
|
|
|
if ( insertLevel < minInsertLevel ) {
|
|
|
|
// Closing an unopened element at a higher
|
|
|
|
// (more negative) level than before
|
|
|
|
// Lazy-initialize scope
|
|
|
|
scope = scope || this.document.getNodeFromOffset( prevCursor );
|
|
|
|
// Push the full range of the old scope as an affected range
|
2012-11-21 20:02:32 +00:00
|
|
|
scopeStart =
|
|
|
|
this.document.getDocumentNode().getOffsetFromNode( scope );
|
2012-07-19 03:40:49 +00:00
|
|
|
scopeEnd = scopeStart + scope.getOuterLength();
|
|
|
|
affectedRanges.push( new ve.Range( scopeStart, scopeEnd ) );
|
|
|
|
// Update scope
|
|
|
|
scope = scope.getParent() || scope;
|
2012-10-23 00:53:58 +00:00
|
|
|
// Set change marker
|
|
|
|
this.transaction.setChangeMarker(
|
|
|
|
scopeStart + this.adjustment,
|
|
|
|
'rebuilt'
|
|
|
|
);
|
2012-07-19 03:40:49 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
|
2012-07-19 03:40:49 +00:00
|
|
|
} else {
|
|
|
|
// Opening element
|
|
|
|
insertLevel++;
|
2012-10-23 00:53:58 +00:00
|
|
|
// Mark as 'created'
|
|
|
|
this.setChangeMarker( prevCursor + i, 'created' );
|
2012-07-19 03:40:49 +00:00
|
|
|
}
|
2012-03-12 03:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Update adjustment
|
|
|
|
this.adjustment += opInsert.length - opRemove.length;
|
2012-06-21 06:30:25 +00:00
|
|
|
opAdjustment += opInsert.length - opRemove.length;
|
2012-03-12 03:55:32 +00:00
|
|
|
} else {
|
2012-06-20 01:20:28 +00:00
|
|
|
// We know that other operations won't cause adjustments, so we
|
|
|
|
// don't have to update adjustment
|
2012-03-12 03:55:32 +00:00
|
|
|
this.executeOperation( operation );
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( removeLevel === 0 && insertLevel === 0 ) {
|
2012-03-12 03:55:32 +00:00
|
|
|
// The model is back in a consistent state, so we're done
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Get the next operation
|
|
|
|
operation = this.nextOperation();
|
|
|
|
if ( !operation ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Unbalanced set of replace operations found' );
|
2012-03-12 03:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// From all the affected ranges we have gathered, compute a range that covers all
|
|
|
|
// of them, and rebuild that
|
|
|
|
coveringRange = ve.Range.newCoveringRange( affectedRanges );
|
2012-06-21 06:30:25 +00:00
|
|
|
this.synchronizer.pushRebuild(
|
|
|
|
coveringRange,
|
|
|
|
new ve.Range(
|
|
|
|
coveringRange.start + this.adjustment - opAdjustment,
|
|
|
|
coveringRange.end + this.adjustment
|
|
|
|
)
|
2012-06-20 01:20:28 +00:00
|
|
|
);
|
2012-03-12 03:55:32 +00:00
|
|
|
}
|
2012-03-08 23:21:17 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the next operation.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.prototype.nextOperation = function () {
|
2012-06-20 01:20:28 +00:00
|
|
|
return this.operations[this.operationIndex++] || false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executes an operation.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Object} op Operation object to execute
|
|
|
|
* @throws 'Invalid operation error. Operation type is not supported'
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.prototype.executeOperation = function ( op ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( op.type in ve.dm.TransactionProcessor.processors ) {
|
|
|
|
ve.dm.TransactionProcessor.processors[op.type].call( this, op );
|
2011-11-14 23:04:36 +00:00
|
|
|
} else {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Invalid operation error. Operation type is not supported: ' + op.type );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Processes all operations.
|
|
|
|
*
|
|
|
|
* When all operations are done being processed, the document will be synchronized.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.prototype.process = function () {
|
2012-06-20 01:20:28 +00:00
|
|
|
var op;
|
2012-10-23 00:53:58 +00:00
|
|
|
if ( this.reversed ) {
|
|
|
|
// Undo change markers before rolling back the transaction, because the offsets
|
|
|
|
// are relevant to the post-commit state
|
|
|
|
this.applyChangeMarkers();
|
|
|
|
// Unset the change markers we've just undone
|
|
|
|
this.transaction.clearChangeMarkers();
|
|
|
|
}
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
// This loop is factored this way to allow operations to be skipped over or executed
|
|
|
|
// from within other operations
|
|
|
|
this.operationIndex = 0;
|
|
|
|
while ( ( op = this.nextOperation() ) ) {
|
|
|
|
this.executeOperation( op );
|
2012-01-23 18:46:31 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
this.synchronizer.synchronize();
|
2012-10-23 00:53:58 +00:00
|
|
|
|
|
|
|
if ( !this.reversed ) {
|
|
|
|
// Apply the change markers we've accumulated while processing the transaction
|
|
|
|
this.applyChangeMarkers();
|
|
|
|
}
|
|
|
|
// Mark the transaction as committed or rolled back, as appropriate
|
2012-10-23 00:28:37 +00:00
|
|
|
this.transaction.toggleApplied();
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Apply the current annotation stacks. This will set all annotations in this.set and clear all
|
|
|
|
* annotations in this.clear on the data between the offsets this.cursor and this.cursor + to
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Number} to Offset to stop annotating at. Annotating starts at this.cursor
|
2012-12-07 21:38:00 +00:00
|
|
|
* @throws 'Invalid transaction, cannot annotate a branch element'
|
2012-06-20 01:20:28 +00:00
|
|
|
* @throws 'Invalid transaction, annotation to be set is already set'
|
|
|
|
* @throws 'Invalid transaction, annotation to be cleared is not set'
|
|
|
|
*/
|
2012-08-02 18:46:13 +00:00
|
|
|
ve.dm.TransactionProcessor.prototype.applyAnnotations = function ( to ) {
|
2012-11-23 21:07:20 +00:00
|
|
|
var item, element, type, annotated, annotations, i, range, selection, offset;
|
2012-08-24 02:06:36 +00:00
|
|
|
if ( this.set.isEmpty() && this.clear.isEmpty() ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
return;
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2012-08-02 18:46:13 +00:00
|
|
|
for ( i = this.cursor; i < to; i++ ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
item = this.document.data[i];
|
|
|
|
element = item.type !== undefined;
|
|
|
|
if ( element ) {
|
2012-11-23 21:07:20 +00:00
|
|
|
type = item.type;
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( item.type.charAt( 0 ) === '/' ) {
|
2012-11-23 21:07:20 +00:00
|
|
|
type = type.substr( 1 );
|
|
|
|
}
|
|
|
|
if ( !ve.dm.nodeFactory.isNodeContent( type ) ) {
|
|
|
|
throw new Error( 'Invalid transaction, cannot annotate a non-content element' );
|
|
|
|
}
|
|
|
|
if ( item.type.charAt( 0 ) === '/' ) {
|
|
|
|
// Closing content element, ignore
|
|
|
|
continue;
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
annotated = element ? 'annotations' in item : ve.isArray( item );
|
2012-08-24 02:06:36 +00:00
|
|
|
annotations = annotated ? ( element ? item.annotations : item[1] ) :
|
|
|
|
new ve.AnnotationSet();
|
2012-06-20 01:20:28 +00:00
|
|
|
// Set and clear annotations
|
2012-08-24 02:06:36 +00:00
|
|
|
if ( annotations.containsAnyOf( this.set ) ) {
|
|
|
|
throw new Error( 'Invalid transaction, annotation to be set is already set' );
|
|
|
|
} else {
|
|
|
|
annotations.addSet( this.set );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
2012-08-24 02:06:36 +00:00
|
|
|
if ( !annotations.containsAllOf( this.clear ) ) {
|
|
|
|
throw new Error( 'Invalid transaction, annotation to be cleared is not set' );
|
|
|
|
} else {
|
|
|
|
annotations.removeSet( this.clear );
|
2011-11-30 19:21:33 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Auto initialize/cleanup
|
2012-08-24 02:06:36 +00:00
|
|
|
if ( !annotations.isEmpty() && !annotated ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( element ) {
|
|
|
|
// Initialize new element annotation
|
2012-08-24 02:06:36 +00:00
|
|
|
item.annotations = new ve.AnnotationSet( annotations );
|
2012-06-20 01:20:28 +00:00
|
|
|
} else {
|
|
|
|
// Initialize new character annotation
|
2012-08-24 02:06:36 +00:00
|
|
|
this.document.data[i] = [item, new ve.AnnotationSet( annotations )];
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
2012-08-24 02:06:36 +00:00
|
|
|
} else if ( annotations.isEmpty() && annotated ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( element ) {
|
|
|
|
// Cleanup empty element annotation
|
|
|
|
delete item.annotations;
|
|
|
|
} else {
|
|
|
|
// Cleanup empty character annotation
|
|
|
|
this.document.data[i] = item[0];
|
|
|
|
}
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
|
|
|
}
|
2012-10-23 00:53:58 +00:00
|
|
|
if ( this.cursor < to ) {
|
|
|
|
range = new ve.Range( this.cursor, to );
|
|
|
|
selection = this.document.selectNodes(
|
|
|
|
new ve.Range(
|
|
|
|
this.cursor - this.adjustment,
|
|
|
|
to - this.adjustment
|
|
|
|
),
|
|
|
|
'leaves'
|
|
|
|
);
|
|
|
|
for ( i = 0; i < selection.length; i++ ) {
|
2012-11-19 22:19:13 +00:00
|
|
|
offset = !selection[i].node.isWrapped() && selection[i].parentOuterRange ?
|
|
|
|
selection[i].parentOuterRange.start :
|
|
|
|
selection[i].nodeOuterRange.start;
|
2012-10-23 00:53:58 +00:00
|
|
|
this.setChangeMarker( offset + this.adjustment, 'annotations' );
|
|
|
|
}
|
|
|
|
this.synchronizer.pushAnnotation( new ve.Range( this.cursor, to ) );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a change marker on our transaction, if we are in commit mode. This function is a no-op in
|
|
|
|
* rollback mode.
|
|
|
|
* @see {ve.dm.Transaction.setChangeMarker}
|
|
|
|
*/
|
|
|
|
ve.dm.TransactionProcessor.prototype.setChangeMarker = function ( offset, type, increment ) {
|
|
|
|
// Refuse to set any new change markers while reversing transactions
|
|
|
|
if ( !this.reversed ) {
|
|
|
|
this.transaction.setChangeMarker( offset, type, increment );
|
|
|
|
}
|
2012-11-26 21:36:07 +00:00
|
|
|
};
|
2012-10-23 00:53:58 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply the change markers on this.transaction to this.document . Change markers are set
|
|
|
|
* (incremented) in commit mode, and unset (decremented) in rollback mode.
|
|
|
|
*/
|
|
|
|
ve.dm.TransactionProcessor.prototype.applyChangeMarkers = function () {
|
|
|
|
var offset, type, previousValue, newValue, element,
|
|
|
|
markers = this.transaction.getChangeMarkers(),
|
|
|
|
m = this.reversed ? -1 : 1;
|
|
|
|
for ( offset in markers ) {
|
|
|
|
for ( type in markers[offset] ) {
|
|
|
|
offset = Number( offset );
|
|
|
|
element = this.document.data[offset];
|
|
|
|
previousValue = ve.getProp( element, 'internal', 'changed', type );
|
|
|
|
newValue = ( previousValue || 0 ) + m*markers[offset][type];
|
2012-11-26 21:36:07 +00:00
|
|
|
if ( newValue !== 0 ) {
|
2012-10-23 00:53:58 +00:00
|
|
|
ve.setProp( element, 'internal', 'changed', type, newValue );
|
|
|
|
} else if ( previousValue !== undefined ) {
|
|
|
|
// Value was set but becomes zero, delete the key
|
|
|
|
delete element.internal.changed[type];
|
|
|
|
// If that made .changed empty, delete it
|
|
|
|
if ( ve.isEmptyObject( element.internal.changed ) ) {
|
|
|
|
delete element.internal.changed;
|
|
|
|
}
|
|
|
|
// If that made .internal empty, delete it
|
|
|
|
if ( ve.isEmptyObject( element.internal ) ) {
|
|
|
|
delete element.internal;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|