Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/VisualEditor/lib/ve/src/dm/ve.dm.TransactionSquasher.js
Ðазад
/*! * VisualEditor DataModel TransactionSquasher class. * * @copyright See AUTHORS.txt */ /** * Squasher to create one transaction from multiple transactions applicable in turn * * The squashed transaction has the same effect on a document as applying the original * transactions in turn, but it may cause rebase conflicts where the original sequence * of transactions would not have. * * Suppose we have linmod arrays A, B, C, D. Note that (x,A->B,m) then (x,C->D,n) becomes * (x,A.concat(C*)->D.concat(B*),min(m,n)) Where len = min(B.length, C.length) and * C*=C.slice(len) and B*=B.slice(len). I.e. "A->B then C->D" becomes "remove A, remove C*, * insert D, insert B*". Examples: * 1234Aa5678 (4,Aa->Bb,4) 1234Bb5678 (4,B->D,5) 1234Db5678 --> (4,Aa->Db,4) * 1234Aa5678 (4,Aa->Bb,4) 1234Bb5678 (4,Bb5->Dd,3) 1234Dd678 --> (4,Aa5->Dd,3) * * The same sort of thing happens if the starts are not the same, e.g. * helloworld (2,ll>LL,1,wo>WO,3) heLLoWOrld (3,LoW>HI,4) heLHIrld --> (2,llowo>LHI,3) * hiwed (1,i>ello,1,e>orl,1) helloworld (2,llowor>a,2) heald --> (1,iwe>eal,1) * * However, removal followed by reinsertion cannot be stripped out entirely, because then * the squashed transaction, considered as a partial function between documents, would have * a larger preimage than the composition of the original transactions. This can probably * break things like associativity). Example: * * hello! (1,ello>iworld,1) hiworld! (1,i>ello,6) helloworld! --> (1,ello>elloworld,1) * * For annotations in the follow-up transaction, two forms of processing are needed: annotating * previously-inserted content, and adding the annotations operation into the transaction for * retained ranges. * * @class * @constructor * @param {ve.dm.Transaction} transaction Base transaction to clone then squash others onto */ ve.dm.TransactionSquasher = function VeDmTransactionSquasher( transaction ) { /** * @property {ve.dm.Transaction} transaction Transaction being squashed together */ this.transaction = transaction.clone(); /** * @property {Object[]} operations Reference to .operations within the tx */ this.operations = this.transaction.operations; /** * @property {Object} op During squashIn, the current op within operations */ this.op = this.operations[ 0 ]; /** * @property {number} index During squashIn, index of current op within operations */ this.index = 0; /** * During squashIn, post-transaction offset within the current op * * @property {number} offset During squashIn, post-tx linmod offset within current op. * "Post transaction" means that for replacements, the offset is within the insert * block. The reason we care about post-transaction offsets is that the match the * pre-transaction offsets of the next transaction. */ this.offset = 0; /** * @property {number} globalOffset During squashIn, global offset over all operations */ this.globalOffset = 0; /** * @property {Object} attributeOperations During squashIn, live references to attribute * operations at current offset, keyed by attribute name, or null if at an open element */ this.attributeOperations = {}; }; /* Inheritance */ OO.initClass( ve.dm.TransactionSquasher ); /* Static methods */ /** * Test whether two linmod items have equal values * * @param {any} item1 A data item * @param {any} item2 Another data item * @return {boolean} Whether the items have equal values */ ve.dm.TransactionSquasher.static.equalItems = function ( item1, item2 ) { function stringifyItem( item ) { return JSON.stringify( item, ( key, value ) => key === 'changesSinceLoad' ? undefined : value ); } // Compare serializations, so that reference types need not be reference-equal return stringifyItem( item1 ) === stringifyItem( item2 ); }; /** * Squash an array of consecutive transactions into a single transaction * * @param {ve.dm.Transaction[]} transactions Non-empty array of consecutive transactions * @return {ve.dm.Transaction} Single transaction with the same content as the transaction array */ ve.dm.TransactionSquasher.static.squash = function ( transactions ) { if ( transactions.length === 0 ) { throw new Error( 'Cannot squash empty transaction array' ); } const squasher = new ve.dm.TransactionSquasher( transactions[ 0 ] ); for ( let i = 1, iLen = transactions.length; i < iLen; i++ ) { squasher.squashIn( transactions[ i ] ); } return squasher.getTransaction(); }; /* Methods */ /** * Get the Transaction as-is * * @return {ve.dm.Transaction} The transaction */ ve.dm.TransactionSquasher.prototype.getTransaction = function () { return this.transaction; }; /** * Modify our Transaction in-place to incorporate a follow-up transaction * * Applying the modified transaction has the same effect as applying the original * transaction then the follow-up, but it may cause rebase conflicts where the original * pair of transactions would not have. * * @param {ve.dm.Transaction} tx Follow-up transaction (that can apply immediately after this) */ ve.dm.TransactionSquasher.prototype.squashIn = function ( tx ) { // Walk over the document offsets in our transaction, modifying operations in-place // to incorporate the operations in tx this.index = 0; this.offset = 0; this.globalOffset = 0; this.op = this.operations[ this.index ]; this.changes = []; this.oldChanges = []; this.readAttributes(); // Do not cache length, as we may splice the list for ( let i = 0; i < tx.operations.length; i++ ) { const op = tx.operations[ i ]; if ( op.type === 'retain' ) { let retainLength = op.length; while ( retainLength > 0 ) { const consumed = this.processRetain( retainLength ); retainLength -= consumed; } } else if ( op.type === 'replace' ) { let items = JSON.parse( JSON.stringify( op.remove ) ); while ( items.length > 0 ) { const consumed = this.processRemove( items ); items.splice( 0, consumed ); } items = JSON.parse( JSON.stringify( op.insert ) ); while ( items.length > 0 ) { const consumed = this.processInsert( items ); items.splice( 0, consumed ); } } else if ( op.type === 'attribute' ) { this.processAttribute( op.key, op.from, op.to ); } else { throw new Error( 'Unknown operation type ' + op.type ); } } }; /** * Process the retention of content, stopping part-way if convenient * * @private * @param {number} maxLength The maximum amount of content to retain * @return {number} The amount of content retained */ ve.dm.TransactionSquasher.prototype.processRetain = function ( maxLength ) { this.normalizePosition(); if ( !this.op ) { throw new Error( 'Past end of transaction' ); } if ( this.op.type === 'retain' ) { const len = Math.min( maxLength, this.op.length - this.offset ); this.offset += len; this.globalOffset += len; this.attributeOperations = {}; return len; } if ( this.op.type === 'replace' ) { // Apply annotation changes to inserted content const len = Math.min( maxLength, this.op.insert.length - this.offset ); // There is never any need to adjust spliceAt, because the splices are always // applied in the same order in which they were generated this.offset += len; this.globalOffset += len; this.readAttributes(); return len; } if ( this.op.type === 'attribute' ) { // Do nothing this.index++; this.op = this.operations[ this.index ]; // By assumption, this.offset is already 0 this.attributeOperations[ this.op.key ] = this.op; return 0; } throw new Error( 'Unknown op type: ' + this.op.type ); }; /** * Process the removal of some items, stopping part-way if convenient * * If some of the removal is undoing an insertion in this.transaction, then the "cancelled" * content is stripped out entirely from the squashed transaction. * * @private * @param {Object[]} items Items to remove some of; can be modified in place (annotated) * @return {number} The length of the initial slice of items that was removed */ ve.dm.TransactionSquasher.prototype.processRemove = function ( items ) { let tryUnsplit = false; this.normalizePosition(); if ( !this.op ) { throw new Error( 'Past end of transaction' ); } if ( this.op.type === 'retain' ) { this.splitIfInterior(); // Now we must be at the start of a retain const len = Math.min( items.length, this.op.length ); this.op.length -= len; if ( this.op.length === 0 ) { tryUnsplit = true; // Remove empty retain this.operations.splice( this.index, 1 ); // this.op may become undefined; ok this.op = this.operations[ this.index ]; } const removal = items.slice( 0, len ); if ( this.offset === 0 && this.op && this.op.type === 'replace' ) { // If we're at the start of a replace op, prepend to it ve.batchSplice( this.op.remove, 0, 0, removal ); } else { // Get the immediately preceding replace op, or insert an empty one let replaceOp = this.operations[ this.index - 1 ]; if ( !replaceOp || replaceOp.type !== 'replace' ) { replaceOp = { type: 'replace', remove: [], insert: [] }; this.operations.splice( this.index, 0, replaceOp ); this.index++; } ve.batchSplice( replaceOp.remove, replaceOp.remove.length, 0, removal ); } if ( tryUnsplit ) { this.tryUnsplit(); } this.readAttributes(); return len; } if ( this.op.type === 'replace' ) { // Check removal against insertion, then cancel them out const len = Math.min( items.length, this.op.insert.length - this.offset ); // len must be greater than zero, since we're not at the end of this op const removal = items.slice( 0, len ); for ( let i = 0; i < len; i++ ) { if ( !this.constructor.static.equalItems( removal[ i ], this.op.insert[ this.offset + i ] ) ) { throw new Error( 'Remove does not match insert' ); } } this.op.insert.splice( this.offset, len ); if ( this.op.remove.length === 0 && this.op.insert.length === 0 ) { // Empty replacement: delete it this.operations.splice( this.index, 1 ); this.op = this.operations[ this.index ]; // By assumption, this.offset is already 0 this.tryUnsplit(); } this.readAttributes(); return len; } if ( this.op.type === 'attribute' ) { // Apply the reversed attribute change to the item being removed this.changeElement( items[ 0 ], this.op.key, this.op.to, this.op.from ); this.operations.splice( this.index, 1 ); this.op = this.operations[ this.index ]; // By assumption, this.offset is already 0 this.tryUnsplit(); return 0; } throw new Error( 'Unknown operation type ' + this.op.type ); }; /** * Process the insertion of some items, stopping part-way if convenient * * If some of the insertion is undoing a removal in this.transaction, then the "cancelled" * content effectively becomes part of an identity replacement: replace 'foo' with 'foo'. * (The content cannot be stripped out entirely from the squashed transaction, because then * the squashed transaction, considered as a partial function between documents, would have * a larger preimage than the composition of the original transactions. This can probably * break things like associativity). * * @private * @param {Object[]} items Items to insert some of * @return {number} The length of the initial slice of items that was inserted */ ve.dm.TransactionSquasher.prototype.processInsert = function ( items ) { this.normalizePosition(); if ( !this.op || this.op.type === 'retain' || this.op.type === 'attribute' ) { if ( this.op && this.op.type === 'retain' ) { // in a retain this.splitIfInterior(); } // We must be at the start of this.op (or at the end if !this.op) // Get the immediately preceding replace op, or insert an empty one let replaceOp = this.operations[ this.index - 1 ]; if ( !replaceOp || replaceOp.type !== 'replace' ) { replaceOp = { type: 'replace', remove: [], insert: [] }; this.operations.splice( this.index, 0, replaceOp ); this.index++; // By hypothesis, this.offset is already zero } ve.batchSplice( replaceOp.insert, replaceOp.insert.length, 0, items ); this.globalOffset += items.length; this.attributeOperations = {}; return items.length; } if ( this.op.type === 'replace' ) { // Do *not* try to cancel insertions against a matching prior removal, // because it would cause the squashed transaction to "forget" prior // linmod content that the original transaction pair requires. This // breaks useful properties like associativity. ve.batchSplice( this.op.insert, this.offset, 0, items ); this.offset += items.length; this.globalOffset += items.length; this.attributeOperations = {}; return items.length; } throw new Error( 'Unknown operation type ' + this.op.type ); }; /** * Process the setting of an attribute * * The case from === to is possible. An identity attribute change still proves there is * an open element at this position, so cannot be stripped * * @private * @param {string} key The attribute key * @param {any} from The old value * @param {any} to The new value */ ve.dm.TransactionSquasher.prototype.processAttribute = function ( key, from, to ) { this.normalizePosition(); if ( this.op && this.op.type === 'replace' && this.offset < this.op.insert.length ) { // If we're at some insert content, then the attribute should apply to it const item = this.op.insert[ this.offset ]; if ( !item || !item.type || item.type[ 0 ] === '/' ) { throw new Error( 'Expected open element' ); } this.changeElement( this.op.insert[ this.offset ], key, from, to ); } else { let op = this.attributeOperations[ key ]; if ( op ) { if ( !this.constructor.static.equalItems( op.to, from ) ) { throw new Error( 'Unexpected prior attribute value' ); } // Modify in place op.to = to; } else { op = { type: 'attribute', key: key, from: from, to: to }; this.splitIfInterior(); this.operations.splice( this.index, 0, op ); this.attributeOperations[ key ] = op; this.index++; } } }; /** * Change an attribute in an open element * * @private * @param {Object} openElement The open element * @param {string} key The attribute name * @param {any} from Old value, or undefined if the attribute is being created * @param {any} to New value, or undefined if the attribute is being removed */ ve.dm.TransactionSquasher.prototype.changeElement = function ( openElement, key, from, to ) { if ( !openElement.attributes ) { openElement.attributes = {}; } if ( !this.constructor.static.equalItems( openElement.attributes[ key ], from ) ) { throw new Error( 'Unexpected prior attribute value' ); } if ( to === undefined ) { delete openElement.attributes[ key ]; if ( !Object.keys( openElement.attributes ).length ) { delete openElement.attributes; } } else { openElement.attributes[ key ] = to; } }; /** * Normalize .index, .offset and .op so we're not at the end of a replace/retain * * @private */ ve.dm.TransactionSquasher.prototype.normalizePosition = function () { while ( this.op && ( ( this.op.type === 'retain' && this.offset === this.op.length ) || ( this.op.type === 'replace' && this.offset === this.op.insert.length ) ) ) { this.index++; this.offset = 0; // op may become undefined; ok this.op = this.operations[ this.index ]; this.readAttributes(); } }; /** * Read the open element at the current offset (if any) * * Sets this.openElement to the open element (or null) * Sets this.attribute to an object containing attribute key-values (or {}) * * @private */ ve.dm.TransactionSquasher.prototype.readAttributes = function () { let index = this.index, op = this.operations[ index ], offset = this.offset; this.attributeOperations = {}; while ( true ) { if ( !op ) { return; } if ( op.type === 'replace' ) { if ( offset < op.insert.length ) { // At an openElement, so there should be no attributeOperations return; } } else if ( op.type === 'attribute' ) { this.attributeOperations[ op.key ] = op; } else if ( op.type === 'retain' && offset < op.length ) { return; } // Else at the start of an insert / retain: step backwards index++; offset = 0; op = this.operations[ index ]; } }; /** * If in the interior of a retain operation, split it here without moving. * * For retain, the length is split at the current offset (throws an error if at the end) * For all other operations, throws an error if not at the start * * Afterwards, this.offset is guaranteed to be 0. * * @private */ ve.dm.TransactionSquasher.prototype.splitIfInterior = function () { const type = this.op && this.op.type; if ( this.offset === 0 ) { // No need to split return; } if ( type !== 'retain' ) { throw new Error( 'Non-zero offset, but op type is ' + type ); } const len = this.op.length; const remainder = len - this.offset; if ( remainder < 0 ) { throw new Error( 'Beyond the end of retain' ); } if ( remainder === 0 ) { throw new Error( 'Cannot split at the end of retain' ); } this.op.length = this.offset; this.index++; this.offset = 0; this.op = { type: 'retain', length: remainder }; this.operations.splice( this.index, 0, this.op ); }; /** * If this operation and the previous one are retains, join them * * @private */ ve.dm.TransactionSquasher.prototype.tryUnsplit = function () { const prevOp = this.operations[ this.index - 1 ]; if ( !this.op || !prevOp ) { return; } if ( prevOp.type === 'retain' && this.op.type === 'retain' ) { this.offset += prevOp.length; prevOp.length += this.op.length; this.operations.splice( this.index, 1 ); this.index--; this.op = prevOp; } else if ( prevOp.type === 'replace' && this.op.type === 'replace' ) { this.offset += prevOp.insert.length; ve.batchSplice( prevOp.remove, prevOp.remove.length, 0, this.op.remove ); ve.batchSplice( prevOp.insert, prevOp.insert.length, 0, this.op.insert ); this.operations.splice( this.index, 1 ); this.index--; this.op = prevOp; } // Else do nothing };
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка