%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/
Upload File :
Create Path :
Current File : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ve.dm.TransactionProcessor.js

/*!
 * VisualEditor DataModel TransactionProcessor class.
 *
 * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
 */

/**
 * 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 {ve.dm.Document#commit} instead.
 *
 * NOTE: Instances of this class are not recyclable: you can only call .process() on them once.
 *
 * @class
 * @param {ve.dm.Document} doc Document
 * @param {ve.dm.Transaction} transaction Transaction
 * @param {boolean} isStaging Transaction is being applied in staging mode
 * @constructor
 */
ve.dm.TransactionProcessor = function VeDmTransactionProcessor( doc, transaction, isStaging ) {
	// Properties
	this.document = doc;
	this.transaction = transaction;
	this.operations = transaction.getOperations();
	this.modificationQueue = [];
	this.rollbackQueue = [];
	this.synchronizer = new ve.dm.DocumentSynchronizer( doc, transaction, isStaging );
	// Linear model offset that we're currently at. Operations in the transaction are ordered, so
	// the cursor only ever moves forward.
	this.cursor = 0;
	this.metadataCursor = 0;
	// Adjustment that needs to be added to linear model offsets in the original linear model
	// to get offsets in the half-updated linear model. This is needed when queueing modifications
	// after other modifications that will cause offsets to shift.
	this.adjustment = 0;
	// Set and clear are sets of annotations which should be added or removed to content being
	// inserted or retained.
	this.set = new ve.dm.AnnotationSet( this.document.getStore() );
	this.clear = new ve.dm.AnnotationSet( this.document.getStore() );
};

/* Static members */

/* See ve.dm.TransactionProcessor.modifiers */
ve.dm.TransactionProcessor.modifiers = {};

/* See ve.dm.TransactionProcessor.processors */
ve.dm.TransactionProcessor.processors = {};

/* Methods */

/**
 * Get the next operation.
 *
 * @method
 */
ve.dm.TransactionProcessor.prototype.nextOperation = function () {
	return this.operations[ this.operationIndex++ ] || false;
};

/**
 * Execute an operation.
 *
 * @method
 * @param {Object} op Operation object to execute
 * @throws {Error} Operation type is not supported
 */
ve.dm.TransactionProcessor.prototype.executeOperation = function ( op ) {
	if ( Object.prototype.hasOwnProperty.call( ve.dm.TransactionProcessor.processors, op.type ) ) {
		ve.dm.TransactionProcessor.processors[ op.type ].call( this, op );
	} else {
		throw new Error( 'Invalid operation error. Operation type is not supported: ' + op.type );
	}
};

/**
 * Process all operations.
 *
 * When all operations are done being processed, the document will be synchronized.
 *
 * @method
 * @param {Function} [presynchronizeHandler] Callback to emit before synchronizing
 */
ve.dm.TransactionProcessor.prototype.process = function ( presynchronizeHandler ) {
	var op;

	// First process each operation to gather modifications in the modification queue.
	// If an exception occurs during this stage, we don't need to do anything to recover,
	// because no modifications were made yet.
	this.operationIndex = 0;
	// This loop is factored this way to allow operations to be skipped over or executed
	// from within other operations
	while ( ( op = this.nextOperation() ) ) {
		this.executeOperation( op );
	}

	// Apply the queued modifications
	try {
		this.applyModifications();
	} catch ( e ) {
		// Restore the linear model to its original state
		this.rollbackModifications();
		// Rethrow the exception
		throw e;
	}
	// Mark the transaction as committed
	this.transaction.markAsApplied();

	// Synchronize the node tree for the modifications we just made
	try {
		if ( presynchronizeHandler ) {
			presynchronizeHandler();
		}
		this.synchronizer.synchronize( this.transaction );
	} catch ( e ) {
		// Restore the linear model to its original state
		this.rollbackModifications();
		// The synchronizer may have left the tree in some sort of weird half-baked state,
		// so rebuild it from scratch
		this.document.rebuildTree();
		// Rethrow the exception
		throw e;
	}

};

/**
 * Queue a modification.
 *
 * For available method names, see ve.dm.ElementLinearData and ve.dm.MetaLinearData.
 *
 * @param {Object} modification Object describing the modification
 * @param {string} modification.type Name of a method in ve.dm.TransactionProcessor.modifiers
 * @param {Array} [modification.args] Arguments to pass to this method
 * @throws {Error} Unrecognized modification type
 */
ve.dm.TransactionProcessor.prototype.queueModification = function ( modification ) {
	if ( typeof ve.dm.TransactionProcessor.modifiers[ modification.type ] !== 'function' ) {
		throw new Error( 'Unrecognized modification type ' + modification.type );
	}
	this.modificationQueue.push( modification );
};

/**
 * Apply all modifications queued through #queueModification, and add their rollback functions
 * to this.rollbackQueue.
 */
ve.dm.TransactionProcessor.prototype.applyModifications = function () {
	var i, len, modifier, modifications = this.modificationQueue;
	this.modificationQueue = [];
	for ( i = 0, len = modifications.length; i < len; i++ ) {
		modifier = ve.dm.TransactionProcessor.modifiers[ modifications[ i ].type ];
		// Add to the beginning of rollbackQueue, because the most recent change needs to
		// be undone first
		this.rollbackQueue.unshift( modifier.apply( this, modifications[ i ].args || [] ) );
	}
};

/**
 * Roll back all modifications that have been applied so far. This invokes the callbacks returned
 * by the modifier functions.
 */
ve.dm.TransactionProcessor.prototype.rollbackModifications = function () {
	var i, len, rollbacks = this.rollbackQueue;
	this.rollbackQueue = [];
	for ( i = 0, len = rollbacks.length; i < len; i++ ) {
		rollbacks[ i ]();
	}
};

/**
 * Advance the main data cursor.
 *
 * @method
 * @param {number} increment Number of positions to increment the cursor by
 */
ve.dm.TransactionProcessor.prototype.advanceCursor = function ( increment ) {
	this.cursor += increment;
	this.metadataCursor = 0;
};

/**
 * 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`. Annotations are set at the highest
 * annotation set offset below which annotations are uniform across the whole range.
 *
 * @method
 * @param {number} to Offset to stop annotating at, annotating starts at this.cursor
 * @throws {Error} Cannot annotate a branch element
 * @throws {Error} Annotation to be set is already set
 * @throws {Error} Annotation to be cleared is not set
 */
ve.dm.TransactionProcessor.prototype.applyAnnotations = function ( to ) {
	var annotationsForOffset, setIndex, isElement, annotations, i, j, jlen;

	function setAndClear( anns, set, clear, index ) {
		if ( anns.containsAnyOf( set ) ) {
			throw new Error( 'Invalid transaction, annotation to be set is already set' );
		} else {
			anns.addSet( set, index );
		}
		if ( !anns.containsAllOf( clear ) ) {
			throw new Error( 'Invalid transaction, annotation to be cleared is not set' );
		} else {
			anns.removeSet( clear );
		}
	}

	if ( this.set.isEmpty() && this.clear.isEmpty() ) {
		return;
	}
	// Set/clear annotations on data
	annotationsForOffset = [];
	for ( i = this.cursor; i < to; i++ ) {
		annotationsForOffset[ i - this.cursor ] = this.document.data.getAnnotationsFromOffset( i );
	}
	// Calculate highest offset below which annotations are uniform across the whole range
	setIndex = ve.getCommonStartSequenceLength(
		annotationsForOffset.map( function ( annotations ) {
			return annotations.storeIndexes;
		} )
	);

	for ( i = this.cursor; i < to; i++ ) {
		isElement = this.document.data.isElementData( i );
		if ( isElement ) {
			if ( !ve.dm.nodeFactory.isNodeContent( this.document.data.getType( i ) ) ) {
				throw new Error( 'Invalid transaction, cannot annotate a non-content element' );
			}
			if ( this.document.data.isCloseElementData( i ) ) {
				// Closing content element, ignore
				continue;
			}
		}
		annotations = this.document.data.getAnnotationsFromOffset( i );
		setAndClear( annotations, this.set, this.clear, setIndex );
		// Store annotation indexes in linear model
		this.queueModification( {
			type: 'annotateData',
			args: [ i + this.adjustment, annotations ]
		} );
	}
	// Set/clear annotations on metadata, but not on the edges of the range
	for ( i = this.cursor + 1; i < to; i++ ) {
		for ( j = 0, jlen = this.document.metadata.getDataLength( i ); j < jlen; j++ ) {
			annotations = this.document.metadata.getAnnotationsFromOffsetAndIndex( i, j );
			setAndClear( annotations, this.set, this.clear );
			this.queueModification( {
				type: 'annotateMetadata',
				args: [ i + this.adjustment, j, annotations ]
			} );
		}
	}
	// Notify the synchronizer
	if ( this.cursor < to ) {
		this.synchronizer.pushAnnotation( new ve.Range( this.cursor + this.adjustment, to + this.adjustment ) );
	}
};

/**
 * Modifier methods.
 *
 * Each method executes a specific type of linear model modification and returns a function that
 * undoes the modification, in case we need to recover the previous linear model state.
 * Methods are called in the context of a transaction processor, so they work similar to normal
 * methods on the object.
 *
 * @class ve.dm.TransactionProcessor.modifiers
 * @singleton
 */

/**
 * Splice data into / out of the data or metadata array.
 *
 * @param {string} type 'data' or 'metadata'
 * @param {number} offset Offset to remove/insert at
 * @param {number} remove Number of elements to remove
 * @param {Array} [insert] Elements to insert
 * @return {Function} Function that undoes the modification
 */
ve.dm.TransactionProcessor.modifiers.splice = function ( type, offset, remove, insert ) {
	var removed, data;
	insert = insert || [];
	data = type === 'metadata' ? this.document.metadata : this.document.data;
	removed = data.batchSplice( offset, remove, insert );
	return function () {
		data.batchSplice( offset, insert.length, removed );
	};
};

/**
 * Splice metadata into / out of the metadata array at a given offset.
 *
 * @param {number} offset Offset whose metadata array to modify
 * @param {number} index Index in that offset's metadata array to remove/insert at
 * @param {number} remove Number of elements to remove
 * @param {Array} [insert] Elements to insert
 * @return {Function} Function that undoes the modification
 */
ve.dm.TransactionProcessor.modifiers.spliceMetadataAtOffset = function ( offset, index, remove, insert ) {
	var removed, metadata;
	insert = insert || [];
	metadata = this.document.metadata;
	removed = metadata.spliceMetadataAtOffset( offset, index, remove, insert );
	return function () {
		metadata.spliceMetadataAtOffset( offset, index, insert.length, removed );
	};
};

/**
 * Set annotations at a given data offset.
 *
 * @param {number} offset Offset in data array
 * @param {ve.dm.AnnotationSet} annotations New set of annotations; overwrites old set
 * @return {Function} Function that undoes the modification
 */
ve.dm.TransactionProcessor.modifiers.annotateData = function ( offset, annotations ) {
	var data = this.document.data,
		oldAnnotations = data.getAnnotationsFromOffset( offset );
	data.setAnnotationsAtOffset( offset, annotations );
	return function () {
		data.setAnnotationsAtOffset( offset, oldAnnotations );
	};
};

/**
 * Set annotations at a given metadata offset and index.
 *
 * @param {number} offset Offset to annotate at
 * @param {number} index Index in that offset's metadata array
 * @param {ve.dm.AnnotationSet} annotations New set of annotations; overwrites old set
 * @return {Function} Function that undoes the modification
 */
ve.dm.TransactionProcessor.modifiers.annotateMetadata = function ( offset, index, annotations ) {
	var metadata = this.document.metadata,
		oldAnnotations = metadata.getAnnotationsFromOffsetAndIndex( offset, index );
	metadata.setAnnotationsAtOffsetAndIndex( offset, index, annotations );
	return function () {
		metadata.setAnnotationsAtOffsetAndIndex( offset, index, oldAnnotations );
	};
};

/**
 * Set an attribute at a given offset.
 *
 * @param {number} offset Offset in data array
 * @param {string} key Attribute name
 * @param {Mixed} value New attribute value
 * @return {Function} Function that undoes the modification
 */
ve.dm.TransactionProcessor.modifiers.setAttribute = function ( offset, key, value ) {
	var data = this.document.data,
		item = data.getData( offset ),
		oldValue = item.attributes && item.attributes[ key ];
	data.setAttributeAtOffset( offset, key, value );
	return function () {
		data.setAttributeAtOffset( offset, key, oldValue );
	};
};

/**
 * 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.
 *
 * @class ve.dm.TransactionProcessor.processors
 * @singleton
 */

/**
 * Execute a retain operation.
 *
 * This method is called within the context of a transaction processor instance.
 *
 * This moves the cursor by op.length and applies annotations to the characters that the cursor
 * moved over.
 *
 * @method
 * @param {Object} op Operation object:
 * @param {number} op.length Number of elements to retain
 */
ve.dm.TransactionProcessor.processors.retain = function ( op ) {
	this.applyAnnotations( this.cursor + op.length );
	this.advanceCursor( op.length );
};

/**
 * Execute a metadata retain operation.
 *
 * This method is called within the context of a transaction processor instance.
 *
 * This moves the metadata cursor by op.length.
 *
 * @method
 * @param {Object} op Operation object:
 * @param {number} op.length Number of elements to retain
 */
ve.dm.TransactionProcessor.processors.retainMetadata = function ( op ) {
	this.metadataCursor += op.length;
};

/**
 * Execute an annotate operation.
 *
 * This method is called within the context of a transaction processor instance.
 *
 * This will add an annotation to or remove an annotation from `this.set` or `this.clear`.
 *
 * @method
 * @param {Object} op Operation object
 * @param {string} op.method Annotation method, either 'set' to add or 'clear' to remove
 * @param {string} op.bias End point of marker, either 'start' to begin or 'stop' to end
 * @param {string} op.annotation Annotation object to set or clear from content
 * @throws {Error} Invalid annotation method
 */
ve.dm.TransactionProcessor.processors.annotate = function ( op ) {
	var target, annotation;
	if ( op.method === 'set' ) {
		target = this.set;
	} else if ( op.method === 'clear' ) {
		target = this.clear;
	} else {
		throw new Error( 'Invalid annotation method ' + op.method );
	}
	annotation = this.document.getStore().value( op.index );
	if ( op.bias === 'start' ) {
		target.push( annotation );
	} else {
		target.remove( annotation );
	}
	// Tree sync is done by applyAnnotations()
};

/**
 * Execute an attribute operation.
 *
 * This method is called within the context of a transaction processor 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.
 *
 * @method
 * @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
 */
ve.dm.TransactionProcessor.processors.attribute = function ( op ) {
	if ( !this.document.data.isElementData( this.cursor ) ) {
		throw new Error( 'Invalid element error, cannot set attributes on non-element data' );
	}
	this.queueModification( {
		type: 'setAttribute',
		args: [ this.cursor + this.adjustment, op.key, op.to ]
	} );

	this.synchronizer.pushAttributeChange(
		this.document.getDocumentNode().getNodeFromOffset( this.cursor + 1 ),
		op.key,
		op.from,
		op.to
	);
};

/**
 * Execute a replace operation.
 *
 * This method is called within the context of a transaction processor 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.
 *
 * @method
 * @param {Object} op Operation object
 * @param {Array} op.remove Linear model data to remove
 * @param {Array} op.insert Linear model data to insert
 */
ve.dm.TransactionProcessor.processors.replace = function ( op ) {
	var node, selection, range,
		remove = op.remove,
		insert = op.insert,
		removeMetadata = op.removeMetadata,
		insertMetadata = op.insertMetadata,
		removeLinearData = new ve.dm.ElementLinearData( this.document.getStore(), remove ),
		insertLinearData = new ve.dm.ElementLinearData( this.document.getStore(), insert ),
		removeIsContent = removeLinearData.isContentData(),
		insertIsContent = insertLinearData.isContentData(),
		removeHasStructure = removeLinearData.containsElementData(),
		insertHasStructure = insertLinearData.containsElementData(),
		operation = op,
		removeLevel = 0,
		insertLevel = 0,
		i,
		type,
		prevCursor,
		affectedRanges = [],
		scope,
		minInsertLevel = 0,
		coveringRange,
		scopeStart,
		scopeEnd,
		opAdjustment = 0,
		opRemove, opInsert, opRemoveMetadata, opInsertMetadata;
	if ( removeIsContent && insertIsContent ) {
		// Content replacement
		// Update the linear model
		this.queueModification( {
			type: 'splice',
			args: [ 'data', this.cursor + this.adjustment, remove.length, insert ]
		} );
		// Keep the meta linear model in sync
		if ( removeMetadata !== undefined ) {
			this.queueModification( {
				type: 'splice',
				args: [
					'metadata',
					this.cursor + this.adjustment,
					removeMetadata.length,
					insertMetadata
				]
			} );
		} else {
			this.queueModification( {
				type: 'splice',
				args: [
					'metadata',
					this.cursor + this.adjustment,
					remove.length, new Array( insert.length )
				]
			} );
		}
		// Get the node containing the replaced content
		selection = this.document.selectNodes(
			new ve.Range(
				this.cursor,
				this.cursor + remove.length
			),
			'leaves'
		);
		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 if (
			!removeHasStructure && !insertHasStructure && remove.length === 0 && insert.length > 0 &&
			selection.length === 1 && node && node.canContainContent() &&
			( selection[ 0 ].indexInNode !== undefined || node.getLength() === 0 )
		) {
			// Text-only addition where a text node didn't exist before. Create one
			this.synchronizer.pushInsertTextNode( node, selection[ 0 ].indexInNode || 0, insert.length - remove.length );
		} else {
			// Replacement is not exclusively text
			// Rebuild all covered nodes
			range = new ve.Range(
				selection[ 0 ].nodeOuterRange.start,
				selection[ selection.length - 1 ].nodeOuterRange.end
			);
			this.synchronizer.pushRebuild( range,
				new ve.Range( range.start + this.adjustment,
					range.end + this.adjustment + insert.length - remove.length )
			);
		}

		// Advance the cursor
		this.advanceCursor( remove.length );
		this.adjustment += insert.length - remove.length;
	} 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 ) {
			if ( operation.type === 'replace' ) {
				opRemove = operation.remove;
				opInsert = operation.insert;
				opRemoveMetadata = operation.removeMetadata;
				opInsertMetadata = operation.insertMetadata;
				// Update the linear model
				this.queueModification( {
					type: 'splice',
					args: [ 'data', this.cursor + this.adjustment, opRemove.length, opInsert ]
				} );
				// Keep the meta linear model in sync
				if ( opRemoveMetadata !== undefined ) {
					this.queueModification( {
						type: 'splice',
						args: [
							'metadata',
							this.cursor + this.adjustment,
							opRemoveMetadata.length,
							opInsertMetadata
						]
					} );
				} else {
					this.queueModification( {
						type: 'splice',
						args: [
							'metadata',
							this.cursor + this.adjustment,
							opRemove.length,
							new Array( opInsert.length )
						]
					} );
				}
				affectedRanges.push( new ve.Range(
					this.cursor,
					this.cursor + opRemove.length
				) );
				prevCursor = this.cursor;
				this.advanceCursor( opRemove.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,
						prevCursor + opRemove.length
					), 'siblings' );
					for ( i = 0; i < selection.length; i++ ) {
						affectedRanges.push( selection[ i ].nodeOuterRange );
					}
				}
				// Walk through the remove and insert data
				// 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;
					if ( type !== undefined ) {
						if ( type.charAt( 0 ) === '/' ) {
							// Closing element
							removeLevel--;
						} else {
							// Opening element
							removeLevel++;
						}
					}
				}
				// 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;
					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.getBranchNodeFromOffset( prevCursor );
								// Push the full range of the old scope as an affected range
								scopeStart = scope.getOffset();
								scopeEnd = scopeStart + scope.getOuterLength();
								affectedRanges.push( new ve.Range( scopeStart, scopeEnd ) );
								// Update scope
								scope = scope.getParent() || scope;
								minInsertLevel--;
							}
						} else {
							// Opening element
							insertLevel++;
						}
					}
				}
				// Update adjustment
				this.adjustment += opInsert.length - opRemove.length;
				opAdjustment += opInsert.length - opRemove.length;
			} else {
				// We know that other operations won't cause adjustments, so we
				// don't have to update adjustment
				this.executeOperation( operation );
			}
			if ( removeLevel === 0 && insertLevel === 0 ) {
				// The model is back in a consistent state, so we're done
				break;
			}
			// Get the next operation
			operation = this.nextOperation();
			if ( !operation ) {
				throw new Error( 'Unbalanced set of replace operations found' );
			}
		}

		// From all the affected ranges we have gathered, compute a range that covers all
		// of them, and rebuild that
		coveringRange = ve.Range.static.newCoveringRange( affectedRanges );
		this.synchronizer.pushRebuild(
			coveringRange,
			new ve.Range(
				coveringRange.start + this.adjustment - opAdjustment,
				coveringRange.end + this.adjustment
			)
		);
	}
};

/**
 * Execute a metadata replace operation.
 *
 * This method is called within the context of a transaction processor instance.
 *
 * @method
 * @param {Object} op Operation object
 * @param {Array} op.remove Metadata to remove
 * @param {Array} op.insert Metadata to insert
 */
ve.dm.TransactionProcessor.processors.replaceMetadata = function ( op ) {
	this.queueModification( {
		type: 'spliceMetadataAtOffset',
		args: [ this.cursor + this.adjustment, this.metadataCursor, op.remove.length, op.insert ]
	} );
	this.metadataCursor += op.insert.length;
};

Zerion Mini Shell 1.0