Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/VisualEditor/lib/ve/src/dm/ve.dm.Change.js
Ðазад
/*! * VisualEditor DataModel Change class. * * @copyright See AUTHORS.txt */ /** * DataModel change. * * A change is a list of transactions to be applied sequentially on top of a certain history * state, together with a set that includes all new store values (annotations and DOM elements) * introduced by those transactions. * * It can be thought of more abstractly as a function f: D1 -> D2 a document in a * specific start state D1, modifying parts of the document to produce a specific end state D2. * * For two changes f: D1 -> D2 and g: D2 -> D3 we define f.concat(g): D1 -> D3 as the change * obtained by applying f then g. By associativity of functions, * a.concat(b.concat(c)) = a.concat(b).concat(c) for any consecutive changes a, b, c. Writing * * x * y := x.concat(y) , * * we have a * (b * c) = (a * b) * c, so we can just write either as a * b * c. * * For a change f: D1 -> D2 we define f.reversed() as the change D2 -> D1 such that * f.concat(f.reversed()) is the identity change D1 -> D1. Writing * * inv(x) := x.reversed() , * * we have f * inv(f) = the identity change on D1 . * * Given two changes f: D1 -> D2 and g: D1 -> D3 , We would like to define f.rebasedOnto(g) * ("f rebased onto g") as a change that maps D3 onto some D4: conceptually, it is f * modified so it can be applied after g. This is a useful concept because it allows changes * written in parallel to be sequenced into a linear order. However, for some changes there * is no reasonable way to do this; e.g. when f and g both change the same word to something * different. In this case we make f.rebasedOnto(g) return null and we say it conflicts. * * Given f: D1 -> D2 , g: D2 -> D3, and x: D1 -> D4, we give three guarantees about rebasing: * * 1. x.rebasedOnto(f) conflicts if and only if f.rebasedOnto(x) conflicts. * 2. If there is no conflict, f.concat(x.rebasedOnto(f)) equals x.concat(f.rebasedOnto(x)). * 3. If there is no conflict, x.rebasedOnto(f).rebasedOnto(g) equals x.rebasedOnto(f * g). * * We can consider a conflicting transaction starting at some document D to be 0: D->null, * and regard any two conflicting transactions starting at D to be equal, and just write 0 * where D1 is clear from context. Then, writing * * x|y := x.rebasedOnto(y), * * we can write our guarantees. Given f: D1 -> D2 , g: D2 -> D3, and x: D1 -> D4: * * 1. Change conflict well definedness: x|f = 0 if and only if f|x = 0. * 2. Change commutativity: f * g|f equals g * f|g . * 3. Rebasing piecewise: if (x|f)|g != 0, then (x|f)|g equals x|(f * g) . * * These guarantees let us reorder non-conflicting changes without affecting the resulting * document. They also let us move in the inverse direction ("rebase under"), from sequential * changes to parallel ones, for if f: D1 -> D2 and g: D2 -> D3, then g|inv(f) * maps from D1 to some D4, and conceptually it is g modified to apply without f having been * applied. * * Note that rebasing piecewise is *not* equivalent for changes that conflict: if a change * conflicts f, it might not conflict with f*g. For example, if x|f = 0 then * * (x|f)|inv(f) = 0 but x|(f * inv(f)) = x. * * @class * @constructor * @param {number} [start] Length of the history stack at change start * @param {ve.dm.Transaction[]} [transactions] Transactions to apply * @param {ve.dm.HashValueStore[]} [stores] For each transaction, a collection of new store items * @param {Object} [selections] For each author ID (key), latest ve.dm.Selection */ ve.dm.Change = function VeDmChange( start, transactions, stores, selections ) { this.start = start || 0; this.transactions = transactions || []; this.store = new ve.dm.HashValueStore(); this.storeLengthAtTransaction = []; if ( stores ) { stores.forEach( ( store ) => { this.store.merge( store ); this.storeLengthAtTransaction.push( this.store.getLength() ); } ); } this.selections = selections || {}; }; /* Static methods */ ve.dm.Change.static = {}; /** * Deserialize a change from a JSONable object * * Store values can be deserialized, or kept verbatim; the latter is an optimization if the * Change object will be rebased and reserialized without ever being applied to a document. * * @param {Object} data Change serialized as a JSONable object * @param {boolean} [preserveStoreValues] Keep store values verbatim instead of deserializing * @param {boolean} [unsafe] Use unsafe deserialization (skipping DOMPurify), used via #unsafeDeserialize * @return {ve.dm.Change} Deserialized change */ ve.dm.Change.static.deserialize = function ( data, preserveStoreValues, unsafe ) { const hasOwn = Object.prototype.hasOwnProperty, getTransactionInfo = this.getTransactionInfo, deserializeValue = this.deserializeValue, selections = {}, transactions = [], // If stores is undefined, create an array of nulls stores = data.stores || data.transactions.map( () => null ); /** * Apply annotations in-place to array of code units * * @param {string[]} items Array of code units * @param {string[]|null} annotations Annotations to apply uniformly, or null */ function annotate( items, annotations ) { if ( !annotations || !annotations.length ) { return; } for ( let j = 0, jLen = items.length; j < jLen; j++ ) { items[ j ] = [ items[ j ], annotations.slice() ]; } } for ( const authorId in data.selections ) { selections[ authorId ] = ve.dm.Selection.static.newFromJSON( data.selections[ authorId ] ); } const deserializeStore = ve.dm.HashValueStore.static.deserialize.bind( null, preserveStoreValues ? ( x ) => x : ( x ) => deserializeValue( x, unsafe ) ); let prevInfo; for ( let i = 0, iLen = data.transactions.length; i < iLen; i++ ) { const txSerialized = data.transactions[ i ]; let tx; if ( typeof txSerialized === 'string' ) { const insertion = txSerialized.split( '' ); annotate( insertion, prevInfo.uniformInsert && prevInfo.uniformInsert.annotations ); tx = new ve.dm.Transaction( [ { type: 'retain', length: prevInfo.end }, { type: 'replace', remove: [], insert: insertion }, { type: 'retain', length: prevInfo.docLength - prevInfo.end } ], prevInfo.authorId ); if ( tx.operations[ 2 ].length === 0 ) { tx.operations.pop(); } } else { tx = ve.dm.Transaction.static.deserialize( txSerialized ); if ( prevInfo && !hasOwn.call( txSerialized, 'authorId' ) ) { tx.authorId = prevInfo.authorId; } } transactions.push( tx ); prevInfo = getTransactionInfo( tx ); } return new ve.dm.Change( data.start, transactions, stores.map( deserializeStore ), selections ); }; /** * Deserialize a change from a JSONable object without sanitizing DOM nodes * * @param {Object} data * @return {ve.dm.Change} Deserialized change */ ve.dm.Change.static.unsafeDeserialize = function ( data ) { return this.deserialize( data, false, true ); }; ve.dm.Change.static.serializeValue = function ( value ) { if ( value instanceof ve.dm.Annotation ) { return { type: 'annotation', value: value.element }; } else if ( Array.isArray( value ) && value[ 0 ] instanceof Node ) { return { type: 'domNodes', value: value.map( ve.getNodeHtml ).join( '' ) }; } else { return { type: 'plain', value: value }; } }; ve.dm.Change.static.deserializeValue = function ( serialized, unsafe ) { if ( serialized.type === 'annotation' ) { return ve.dm.annotationFactory.createFromElement( serialized.value ); } else if ( serialized.type === 'domNodes' ) { if ( unsafe ) { // We can use jQuery here because unsafe sanitization // only happens in browser clients. // eslint-disable-next-line no-undef return $.parseHTML( serialized.value, undefined, true ); } else { // Convert NodeList to Array return Array.prototype.slice.call( ve.sanitizeHtml( serialized.value ) ); } } else if ( serialized.type === 'plain' ) { return serialized.value; } else { throw new Error( 'Unrecognized type: ' + serialized.type ); } }; /** * Rebase parallel transactions transactionA and transactionB onto each other * * Recalling that a transaction is a mapping from one ve.dm.ElementLinearData state to another, * suppose we have two parallel transactions, i.e.: * * - transactionA mapping docstate0 to some docstateA, and * - transactionB mapping docstate0 to some docstateB . * * Then we want rebasing to give us two new transactions: * * - aRebasedOntoB mapping docstateB to some docstateC, and * - bRebasedOntoA mapping docstateA to docstateC , * * so that applying transactionA then bRebasedOntoA results in the same document state as * applying transactionB then aRebasedOntoB . * * However, it is useful to regard some transaction pairs as "conflicting" or unrebasable. In * this implementation, transactions are considered to conflict if they have active ranges that * overlap, where a transaction's "active range" means the smallest single range in the *start* * document outside which the contents are unchanged by the transaction. (In practice the * operations within the transaction actually specify which ranges map to where, giving a * natural and unambiguous definition of "active range". Also, the identity transaction on a * document state has no active range but is trivially rebasable with any parallel * transaction). * * For non-conflicting transactions, rebasing of each transaction is performed by resizing the * inactive range either before or after the transaction to accommodate the length difference * caused by the other transaction. There is ambiguity in the case where both transactions have * a zero-length active range at the same position (i.e. two inserts in the same place); in this * case, transactionA's insertion is put before transactionB's. * * It is impossible for rebasing defined this way to create an invalid transaction that breaks * tree validity. This is clear because every position in the rebased transaction's active * range has the same node ancestry as the corresponding position before the rebase (else a * tag must have changed both before and after that position, contradicting the fact that the * transactions' active ranges do not overlap). * * Also it is clear that for a pair of non-conflicting parallel transactions, applying either * one followed by the other rebased will result in the same final document state, as required. * * @param {ve.dm.Transaction} transactionA Transaction A * @param {ve.dm.Transaction} transactionB Transaction B, with the same document start state * @return {any[]} [ aRebasedOntoB, bRebasedOntoA ], or [ null, null ] if conflicting */ ve.dm.Change.static.rebaseTransactions = function ( transactionA, transactionB ) { transactionA = transactionA.clone(); transactionB = transactionB.clone(); const infoA = transactionA.getActiveRangeAndLengthDiff(); const infoB = transactionB.getActiveRangeAndLengthDiff(); if ( infoA.start === undefined || infoB.start === undefined ) { // One of the transactions is a no-op: only need to adjust its retain length. // We can safely adjust both, because the no-op must have diff 0 transactionA.adjustRetain( 'start', infoB.diff ); transactionB.adjustRetain( 'start', infoA.diff ); } else if ( infoA.end <= infoB.start ) { // This includes the case where both transactions are insertions at the same // point transactionB.adjustRetain( 'start', infoA.diff ); transactionA.adjustRetain( 'end', infoB.diff ); } else if ( infoB.end <= infoA.start ) { transactionA.adjustRetain( 'start', infoB.diff ); transactionB.adjustRetain( 'end', infoA.diff ); } else { // The active ranges overlap: conflict return [ null, null ]; } return [ transactionA, transactionB ]; }; /** * @typedef {Object} RebasedChange * @memberof ve.dm.Change * @property {ve.dm.Change} rebased Rebase onto history of uncommitted (or an initial segment of it) * @property {ve.dm.Change} transposedHistory Rebase of history onto initial segment of uncommitted * @property {ve.dm.Change|null} rejected Unrebasable final segment of uncommitted */ /** * Rebase a change on top of a parallel committed one * * Since a change is a stack of transactions, we define change rebasing in terms of transaction * rebasing. We require transaction rebasing to meet the three guarantees described above for * change rebasing. To be precise, given any transactions a:D1->D2, b:D2->D3 and x:D1->D4, we * require that: * * 1. Transaction conflict well definedness: a|x = 0 if and only if x|a = 0. * 2. Transaction commutativity: a * x|a equals x * a|x . * 3. Rebasing piecewise: if (x|a)|b != 0, then (x|a)|b equals x|(a * b) . * * Given committed history consisting of transactions a1,a2,…,aN, and an uncommitted update * consisting of transactions b1,b2,…,bM, our approach is to rebase the whole list a1,…,aN * over b1, and at the same time rebase b1 onto a1*…*aN. * Then we repeat the process for b2, and so on. To rebase a1,…,aN over b1, the following * approach would work: * * a1' := a1|b1 * a2' := a2|(inv(a1) * b1 * a1') * a3' := a3|(inv(a2) * inv(a1) * b1 * a1' * a2') * ⋮ * * That is, rebase a_i under a_i-1,…,a_1, then over b1,…,bM, then over a'1,…,a_i-1' . * * However, because of the way transactions are written, it's not actually easy to implement * transaction concatenation, so we would want to calculate a2' as piecewise rebases * * a2' = ((a2|inv(a1))|b1)|a1' * * which is unsatisfactory because a2|inv(a1) may well conflict even if a2|(inv(a1) * b1 * a1') * as a whole would not conflict (e.g. if b1 modifies only parts of the document distant from a1 * and a2). * * So observe that by transaction commutivity we can rewrite a2' as: * * a2' := a2|(inv(a1) * a1 * b1|a1) * = a2|(b1|a1) * * and that b1|a1 conflicts only if a1|b1 conflicts (so this introduces no new conflicts). In * general we can write: * * a1' := a1|b1 * b1' := b1|a1 * a2' := a2|b1' * b1'' := b1'|a2 * a3' := a3|b1'' * b1''' := a1''|a3 * * Continuing in this way, we obtain a1',…,aN' rebased over b1, and b1''''''' (N primes) * rebased onto a1 * … * aN . Iteratively we can take the same approach to rebase over * b2,…,bM, giving both rebased lists as required. * * If any of the transaction rebases conflict, then we rebase the largest possible * non-conflicting initial segment b1,…,bK onto all of a1,…,aN (so clearly K < M). * * If there are two parallel inserts at the same location, then ordering is ambiguous. We * resolve this by putting the insert for the transaction with the highest author ID * first (Javascript less-than is used, so comparisons with a null author ID do not fail). * If the author IDs are the same, then A's insertion is put before B's. * * @param {ve.dm.Change} history Committed history * @param {ve.dm.Change} uncommitted New transactions, with same start as history * @return {ve.dm.Change.RebasedChange} */ ve.dm.Change.static.rebaseUncommittedChange = function ( history, uncommitted ) { if ( history.start !== uncommitted.start ) { throw new Error( 'Different starts: ' + history.start + ' and ' + uncommitted.start ); } let transactionsA = history.transactions.slice(); const transactionsB = uncommitted.transactions.slice(); let storesA = history.getStores(); const storesB = uncommitted.getStores(); const selectionsA = ve.cloneObject( history.selections ); let selectionsB = ve.cloneObject( uncommitted.selections ); let rejected = null; // For each element b_i of transactionsB, rebase the whole list transactionsA over b_i. // To rebase a1, a2, a3, …, aN over b_i, first we rebase a1 onto b_i. Then we rebase // a2 onto some b', defined as // // b_i' := b_i|a1 , that is b_i.rebasedOnto(a1) // // (which as proven above is equivalent to inv(a1) * b_i * a1) // // Similarly we rebase a3 onto b_i'' := b_i'|a2, and so on. // // The rebased a_j are used for the transposed history: they will all get rebased over the // rest of transactionsB in the same way. // The fully rebased b_i forms the i'th element of the rebased transactionsB. // // If any rebase b_i|a_j fails, we stop rebasing at b_i (i.e. finishing with b_{i-1}). // We return // - rebased: (uncommitted sliced up to i) rebased onto history // - transposedHistory: history rebased onto (uncommitted sliced up to i) // - rejected: uncommitted sliced from i onwards bLoop: for ( let i = 0, iLen = transactionsB.length; i < iLen; i++ ) { let b = transactionsB[ i ]; let storeB = storesB[ i ]; const rebasedTransactionsA = []; const rebasedStoresA = []; for ( let j = 0, jLen = transactionsA.length; j < jLen; j++ ) { const a = transactionsA[ j ]; const storeA = storesA[ j ]; let rebases; if ( b.authorId < a.authorId ) { rebases = ve.dm.Change.static.rebaseTransactions( b, a ).reverse(); } else { rebases = ve.dm.Change.static.rebaseTransactions( a, b ); } if ( rebases[ 0 ] === null ) { rejected = uncommitted.mostRecent( uncommitted.start + i ); transactionsB.length = i; storesB.length = i; selectionsB = {}; break bLoop; } rebasedTransactionsA[ j ] = rebases[ 0 ]; rebasedStoresA[ j ] = storeA.difference( storeB ); b = rebases[ 1 ]; storeB = storeB.difference( storeA ); } transactionsA = rebasedTransactionsA; storesA = rebasedStoresA; transactionsB[ i ] = b; storesB[ i ] = storeB; } // Length calculations below assume no removal of empty rebased transactions const rebased = new ve.dm.Change( uncommitted.start + transactionsA.length, transactionsB, storesB, {} ); const transposedHistory = new ve.dm.Change( history.start + transactionsB.length, transactionsA, storesA, {} ); let authorId; for ( authorId in selectionsB ) { authorId = +authorId; rebased.selections[ authorId ] = selectionsB[ authorId ].translateByChange( transposedHistory, authorId ); } for ( authorId in selectionsA ) { authorId = +authorId; transposedHistory.selections[ authorId ] = selectionsA[ authorId ].translateByChange( rebased, authorId ); } return { rebased: rebased, transposedHistory: transposedHistory, rejected: rejected }; }; /** * @typedef UniformTextInfo * @memberof ve.dm.Change * @property {string} text The code units, in a single string * @property {string} annotations Annotation hashes for all text * @property {string} annotationString Comma-separated annotation hashes */ /** * @typedef TransactionInfo * @memberof ve.dm.Change * @property {number} start The start offset of the replacement * @property {number} end The end offset of the replacement (after replacement) * @property {number} docLength The total length of the document (after replacement) * @property {number} authorId The author ID * @property {ve.dm.Change.UniformTextInfo|null} uniformInsert The insertion as uniform text, or null if not */ /** * Get info about a transaction if it is a "simple replacement", or null if not * * A simple replacement transaction is one that has just one retain op * * @param {ve.dm.Transaction} tx The transaction * @return {ve.dm.Change.TransactionInfo|null} Info about the transaction if a simple replacement, else null */ ve.dm.Change.static.getTransactionInfo = function ( tx ) { // Copy of ve.dm.ElementLinearData.static.getAnnotationHashesFromItem, but we // don't want to load all of ElementLinearData and its dependencies on the server-side. function getAnnotations( item ) { if ( typeof item === 'string' ) { return []; } else if ( item.annotations ) { return item.annotations.slice(); } else if ( item[ 1 ] ) { return item[ 1 ].slice(); } else { return []; } } /** * Get an item's single code unit (without annotation), or null if not a code unit * * @param {Object|Array|string} item The item * @return {string|null} The single code unit, or null if not a code unit */ function getSingleCodeUnit( item ) { if ( typeof item === 'string' && item.length === 1 ) { return item; } if ( Array.isArray( item ) && item[ 0 ].length === 1 ) { return item[ 0 ]; } return null; } /** * Get info about the "uniform text" from an item array, or null if not uniform text * * The item array is uniform text if all items have the same annotations, and * every item is a single code unit of text * * @param {Array} items The items * @return {ve.dm.Change.UniformTextInfo|null} Info about the uniform text, or null if not uniform text */ function getUniformText( items ) { const codeUnits = []; if ( items.length === 0 ) { return null; } let codeUnit = getSingleCodeUnit( items[ 0 ] ); if ( codeUnit === null ) { return null; } codeUnits.push( codeUnit ); const annotations = getAnnotations( items[ 0 ] ); const annotationString = annotations.join( ',' ); for ( let i = 1, iLen = items.length; i < iLen; i++ ) { codeUnit = getSingleCodeUnit( items[ i ] ); if ( codeUnit === null ) { return null; } codeUnits.push( codeUnit ); if ( annotationString !== getAnnotations( items[ i ] ).join( ',' ) ) { return null; } } return { text: codeUnits.join( '' ), annotations: annotations, annotationString: annotationString }; } const op0 = tx.operations[ 0 ]; const op1 = tx.operations[ 1 ]; const op2 = tx.operations[ 2 ]; let replaceOp, start, end, docLength; if ( op0 && op0.type === 'replace' && ( !op1 || op1.type === 'retain' ) && !op2 ) { replaceOp = op0; start = 0; end = start + replaceOp.insert.length; docLength = end; } else if ( op0 && op0.type === 'retain' && op1 && op1.type === 'replace' && ( !op2 || op2.type === 'retain' ) ) { replaceOp = op1; start = op0.length; end = start + replaceOp.insert.length; docLength = end + ( op2 ? op2.length : 0 ); } else { return null; } return { start: start, end: end, docLength: docLength, authorId: tx.authorId, uniformInsert: getUniformText( replaceOp.insert ) }; }; /* Methods */ /** * Create a clone of this Change * * @return {ve.dm.Change} Clone of this change */ ve.dm.Change.prototype.clone = function () { return this.constructor.static.unsafeDeserialize( this.toJSON() ); }; /** * @return {boolean} True if this change has no transactions or selections */ ve.dm.Change.prototype.isEmpty = function () { return this.transactions.length === 0 && Object.keys( this.selections ).length === 0; }; /** * @return {number} The number of transactions */ ve.dm.Change.prototype.getLength = function () { return this.transactions.length; }; /** * Get the store items introduced by transaction n * * @param {number} n The index of a transaction within the change * @return {ve.dm.HashValueStore} The store items introduced by transaction n */ ve.dm.Change.prototype.getStore = function ( n ) { return this.store.slice( n > 0 ? this.storeLengthAtTransaction[ n - 1 ] : 0, this.storeLengthAtTransaction[ n ] ); }; /** * Get the stores for each transaction * * @return {ve.dm.HashValueStore[]} Each transaction's store items (shallow copied store) */ ve.dm.Change.prototype.getStores = function () { const stores = []; let start = 0; for ( let i = 0, len = this.getLength(); i < len; i++ ) { const end = this.storeLengthAtTransaction[ i ]; stores.push( this.store.slice( start, end ) ); start = end; } return stores; }; /** * @return {number|null} The first author in a transaction or selection change, or null if empty */ ve.dm.Change.prototype.firstAuthorId = function () { if ( this.transactions.length ) { return this.transactions[ 0 ].authorId; } const authors = Object.keys( this.selections ); if ( authors.length ) { return +authors[ 0 ]; } return null; }; /** * Get a human-readable summary of the change * * @return {string} Human-readable summary */ ve.dm.Change.prototype.summarize = function () { return '{ start: ' + this.start + ', txs: [ ' + this.transactions.map( ( tx ) => tx.summarize() ).join( ', ' ) + ' ] }'; }; /** * Get the change that backs out this change. * * Note that applying it will not revert start or remove stored items * * @return {ve.dm.Change} The change that backs out this change */ ve.dm.Change.prototype.reversed = function () { return new ve.dm.Change( this.start + this.transactions.length, this.transactions.map( ( tx ) => ve.dm.Transaction.prototype.reversed.call( tx ) ).reverse(), // Empty store for each transaction (reverting cannot possibly add new annotations) this.transactions.map( () => new ve.dm.HashValueStore() ), {} ); }; /** * Rebase this change onto other (ready to apply on top of other) * * @param {ve.dm.Change} other Other change * @return {ve.dm.Change|null} Rebased change applicable on top of other, or null if rebasing fails * @throws {Error} If this change and other have different starts */ ve.dm.Change.prototype.rebasedOnto = function ( other ) { const rebases = this.constructor.static.rebaseUncommittedChange( other, this ); return rebases.rejected ? null : rebases.rebased; }; /** * Build a composite change from two consecutive changes * * @param {ve.dm.Change} other Change that starts immediately after this * @return {ve.dm.Change} Composite change * @throws {Error} If other does not start immediately after this */ ve.dm.Change.prototype.concat = function ( other ) { if ( other.start !== this.start + this.transactions.length ) { throw new Error( 'this ends at ' + ( this.start + this.transactions.length ) + ' but other starts at ' + other.start ); } return new ve.dm.Change( this.start, this.transactions.concat( other.transactions ), this.getStores().concat( other.getStores() ), other.selections ); }; /** * Push a transaction, after having grown the hash value store if required * * @param {ve.dm.Transaction} transaction The transaction * @param {number} storeLength The corresponding store length required */ ve.dm.Change.prototype.pushTransaction = function ( transaction, storeLength ) { if ( typeof storeLength !== 'number' ) { throw new Error( 'Expected numerical storeLength argument, not ' + storeLength ); } this.transactions.push( transaction ); this.storeLengthAtTransaction.push( storeLength ); }; /** * Push another change onto this change * * @param {ve.dm.Change} other Change that starts immediately after this * @throws {Error} If other does not start immediately after this */ ve.dm.Change.prototype.push = function ( other ) { if ( other.start !== this.start + this.getLength() ) { throw new Error( 'this ends at ' + ( this.start + this.getLength() ) + ' but other starts at ' + other.start ); } const stores = other.getStores(); for ( let i = 0, iLen = other.transactions.length; i < iLen; i++ ) { const transaction = other.transactions[ i ]; const store = stores[ i ]; this.store.merge( store ); this.pushTransaction( transaction, this.store.getLength() ); } this.selections = ve.cloneObject( other.selections ); }; /** * Build a composite change from two parallel changes * * @param {ve.dm.Change} other Change parallel to this * @return {ve.dm.Change} Composite change * @throws {Error} If this change and other have different starts */ ve.dm.Change.prototype.concatRebased = function ( other ) { return this.concat( other.rebasedOnto( this ) ); }; /** * Build a change from the last (most recent) transactions * * @param {number} start Start offset * @return {ve.dm.Change} Subset of this change with only the most recent transactions */ ve.dm.Change.prototype.mostRecent = function ( start ) { if ( arguments.length > 1 ) { throw new Error( 'storeStart is no longer needed' ); } return new ve.dm.Change( start, this.transactions.slice( start - this.start ), this.getStores().slice( start - this.start ), ve.cloneObject( this.selections ) ); }; /** * Build a change from the first (least recent) transactions of this change. * * Always removes selections. * * @param {number} length Number of transactions * @return {ve.dm.Change} Subset of this change with only the least recent transactions */ ve.dm.Change.prototype.truncate = function ( length ) { if ( arguments.length > 1 ) { throw new Error( 'storeLength is no longer needed' ); } return new ve.dm.Change( this.start, this.transactions.slice( 0, length ), this.getStores().slice( 0, length ), {} ); }; /** * Apply change to surface * * @param {ve.dm.Surface} surface Surface in change start state * @param {boolean} [applySelection] Apply a selection based on the modified range */ ve.dm.Change.prototype.applyTo = function ( surface, applySelection ) { const doc = surface.getDocument(); if ( this.start !== doc.completeHistory.getLength() ) { throw new Error( 'Change starts at ' + this.start + ', but doc is at ' + doc.completeHistory.getLength() ); } this.getStores().forEach( ( store ) => { doc.store.merge( store ); } ); // Isolate other users' changes from ours with a breakpoint surface.breakpoint(); this.transactions.forEach( ( tx ) => { surface.change( tx ); // Don't mark as applied: this.start already tracks this tx.applied = false; // TODO: This would be better fixed by T202730 if ( applySelection ) { const range = tx.getModifiedRange( doc ); // If the transaction only touched the internal list, there is no modified range within the main document if ( range ) { const offset = doc.getNearestCursorOffset( range.end, -1 ); if ( offset !== -1 ) { surface.setSelection( new ve.dm.LinearSelection( new ve.Range( offset ) ) ); } } } } ); surface.breakpoint(); }; /** * Unapply change to surface, including truncating history and store * * @param {ve.dm.Surface} surface Surface in change end state */ ve.dm.Change.prototype.unapplyTo = function ( surface ) { const doc = surface.getDocument(), historyLength = doc.completeHistory.getLength() - this.getLength(); if ( this.start !== historyLength ) { throw new Error( 'Invalid start: change starts at ' + this.start + ', but doc would be at ' + historyLength ); } this.transactions.slice().reverse().forEach( ( tx ) => { surface.change( tx.reversed() ); } ); doc.completeHistory.transactions.length = historyLength; doc.completeHistory.storeLengthAtTransaction.length = historyLength; doc.store.truncate( doc.completeHistory.storeLengthAtTransaction[ historyLength - 1 ] ); }; /** * Append change transactions to history * * @param {ve.dm.Document} documentModel * @throws {Error} If this change does not start at the top of the history */ ve.dm.Change.prototype.addToHistory = function ( documentModel ) { documentModel.completeHistory.push( this ); }; /** * Remove change transactions from history * * @param {ve.dm.Document} doc * @throws {Error} If this change does not end at the top of the history */ ve.dm.Change.prototype.removeFromHistory = function ( doc ) { if ( this.start + this.getLength() !== doc.completeHistory.getLength() ) { throw new Error( 'this ends at ' + ( this.start + this.getLength() ) + ' but history ends at ' + doc.completeHistory.getLength() ); } doc.completeHistory.transactions.length -= this.transactions.length; doc.completeHistory.storeLengthAtTransaction.length -= this.transactions.length; doc.store.truncate( doc.completeHistory.storeLengthAtTransaction[ doc.completeHistory.getLength() - 1 ] ); }; /** * Serialize the change to a JSONable object * * Store values can be serialized, or kept verbatim (which only makes sense if they are serialized * already, i.e. the Change object was created by #deserialize without deserializing store values). * * @param {boolean} [preserveStoreValues] If true, keep store values verbatim instead of serializing * @return {Object} JSONable object */ ve.dm.Change.prototype.serialize = function ( preserveStoreValues ) { const getTransactionInfo = this.constructor.static.getTransactionInfo, selections = {}, transactions = []; // Recursively serialize, so this method is the inverse of deserialize // without having to use JSON.stringify (which is also recursive). for ( const authorId in this.selections ) { selections[ authorId ] = this.selections[ authorId ].toJSON(); } const serializeStoreValues = preserveStoreValues ? function noop( x ) { return x; } : this.constructor.static.serializeValue; const serializeStore = function ( store ) { return store.serialize( serializeStoreValues ); }; let prevInfo; for ( let i = 0, iLen = this.transactions.length; i < iLen; i++ ) { const tx = this.transactions[ i ]; const info = getTransactionInfo( tx ); if ( info && prevInfo && info.authorId === prevInfo.authorId && info.start === prevInfo.end && info.uniformInsert && prevInfo.uniformInsert && info.uniformInsert.annotationString === prevInfo.uniformInsert.annotationString ) { transactions.push( info.uniformInsert.text ); } else { const txSerialized = tx.toJSON(); if ( i > 0 && tx.authorId === this.transactions[ i - 1 ].authorId ) { delete txSerialized.authorId; } transactions.push( txSerialized ); } prevInfo = info; } const stores = this.getStores().map( serializeStore ); const data = { start: this.start, transactions: transactions }; // Only set stores if at least one is non-null if ( stores.some( ( store ) => store !== null ) ) { data.stores = stores; } if ( Object.keys( selections ).length ) { data.selections = selections; } return data; }; /** * Called automatically by JSON.stringify, see #serialize. * * @param {string} [key] Key in parent object * @return {Object} JSONable object */ ve.dm.Change.prototype.toJSON = function () { // Ensure no native arguments are passed through to #serialize. return this.serialize(); }; /** * Get a Change with all this Change's Transactions compacted into one (or zero) * * The Change has the same effect when applied as this Change does, but it may cause * rebase conflicts where this change does not. * * TODO: introduce a "histLength" feature so the new change can be considered as * having length > 1. * * @return {ve.dm.Change} One-Transaction version of this Change (or empty change) */ ve.dm.Change.prototype.squash = function () { if ( this.transactions.length <= 1 ) { return this.clone(); } return new ve.dm.Change( this.start, [ ve.dm.TransactionSquasher.static.squash( this.transactions ) ], [ this.store.slice() ], // Shallow clone (the individual selections are immutable so need no cloning) ve.cloneObject( this.selections ) ); };
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка