%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ |
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; };