Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/VisualEditor/lib/ve/src/dm/ve.dm.TransactionBuilder.js
Ðазад
/*! * VisualEditor DataModel Transaction builder class. * * @copyright See AUTHORS.txt */ /** * Transaction builder: build transactions based on current ve.dm.Document state * * @class * @constructor */ ve.dm.TransactionBuilder = function VeDmTransactionBuilder() { // TODO: rewrite to store the current ve.dm.Document as a property this.transaction = new ve.dm.Transaction(); }; /* Inheritance */ OO.initClass( ve.dm.TransactionBuilder ); /* Static Methods */ /** * Generate a transaction that replaces data in a range. * * @param {ve.dm.Document} doc Document in pre-transaction state * @param {ve.Range} range Range of data to remove * @param {Array} data Data to insert * @param {boolean} [removeMetadata=false] Remove metadata instead of collapsing it * @return {ve.dm.Transaction} Transaction that replaces data * @throws {Error} Invalid range */ ve.dm.TransactionBuilder.static.newFromReplacement = function ( doc, range, data, removeMetadata ) { const txBuilder = new ve.dm.TransactionBuilder(); let endOffset = txBuilder.pushRemoval( doc, 0, range, removeMetadata ); endOffset = txBuilder.pushInsertion( doc, endOffset, endOffset, data ); txBuilder.pushFinalRetain( doc, endOffset ); return txBuilder.getTransaction(); }; /** * Generate a transaction that inserts data at an offset. * * @static * @param {ve.dm.Document} doc Document in pre-transaction state * @param {number} offset Offset to insert at * @param {Array} data Data to insert * @return {ve.dm.Transaction} Transaction that inserts data */ ve.dm.TransactionBuilder.static.newFromInsertion = function ( doc, offset, data ) { const txBuilder = new ve.dm.TransactionBuilder(), endOffset = txBuilder.pushInsertion( doc, 0, offset, data ); // Retain to end of document, if needed (for completeness) txBuilder.pushFinalRetain( doc, endOffset ); return txBuilder.getTransaction(); }; /** * Generate a transaction that removes data from a range. * * There are three possible results from a removal: * * - Remove content only * - Occurs when the range starts and ends on elements of different type, depth or ancestry * - Remove entire elements and their content * - Occurs when the range spans across an entire element * - Merge two elements by removing the end of one and the beginning of another * - Occurs when the range starts and ends on elements of similar type, depth and ancestry * * This function uses the following logic to decide what to actually remove: * * 1. Elements are only removed if range being removed covers the entire element * 2. Elements can only be merged if {@link ve.dm.Node#canBeMergedWith} returns true * 3. Merges take place at the highest common ancestor * * @param {ve.dm.Document} doc Document in pre-transaction state * @param {ve.Range} range Range of data to remove * @param {boolean} [removeMetadata=false] Remove metadata instead of collapsing it * @return {ve.dm.Transaction} Transaction that removes data * @throws {Error} Invalid range */ ve.dm.TransactionBuilder.static.newFromRemoval = function ( doc, range, removeMetadata ) { const txBuilder = new ve.dm.TransactionBuilder(); let endOffset = txBuilder.pushRemoval( doc, 0, range, removeMetadata ); // Ensure no transaction leaves the document in a completely empty state if ( range.start === 0 && range.end >= doc.getDocumentRange().end ) { endOffset = txBuilder.pushInsertion( doc, endOffset, endOffset, [ { type: 'paragraph' }, { type: '/paragraph' } ] ); } // Retain to end of document, if needed (for completeness) txBuilder.pushFinalRetain( doc, endOffset ); return txBuilder.getTransaction(); }; /** * Build a transaction that inserts the contents of a document at a given offset. * * This is typically used to merge changes to a document slice back into the main document. If newDoc * is a document slice of doc, it's assumed that there were no changes to doc's internal list since * the slice, so any differences between internal items that doc and newDoc have in common will * be resolved in newDoc's favor. * * @param {ve.dm.Document} doc Main document in the state to which the transaction start applies * @param {number} offset Offset to insert at * @param {ve.dm.Document} newDoc Document to insert * @param {ve.Range} [newDocRange] Range from the new document to insert (defaults to entire document) * @return {ve.dm.Transaction} Transaction that inserts the nodes and updates the internal list */ ve.dm.TransactionBuilder.static.newFromDocumentInsertion = function ( doc, offset, newDoc, newDocRange ) { const listNode = doc.internalList.getListNode(), listNodeRange = listNode.getRange(), newListNode = newDoc.internalList.getListNode(), newListNodeRange = newListNode.getRange(), newListNodeOuterRange = newListNode.getOuterRange(); let data; if ( newDocRange ) { data = new ve.dm.ElementLinearData( doc.getStore(), newDoc.getData( newDocRange, true ) ); } else { // Get the data, but skip over the internal list data = new ve.dm.ElementLinearData( doc.getStore(), newDoc.getData( new ve.Range( 0, newListNodeOuterRange.start ), true ).concat( newDoc.getData( new ve.Range( newListNodeOuterRange.end, newDoc.data.getLength() ), true ) ) ); } // Merge the stores doc.getStore().merge( newDoc.getStore() ); const listMerge = doc.internalList.merge( newDoc.internalList, newDoc.origInternalListLength || 0 ); // Remap the indexes in the data data.remapInternalListIndexes( listMerge.mapping, doc.internalList ); // Get data for the new internal list let linearData, listData; if ( newDoc.origInternalListLength !== null ) { // newDoc is a document slice based on doc, so all the internal list items present in doc // when it was cloned are also in newDoc. We need to get the newDoc version of these items // so that changes made in newDoc are reflected. let oldEndOffset, newEndOffset; if ( newDoc.origInternalListLength > 0 ) { oldEndOffset = doc.internalList.getItemNode( newDoc.origInternalListLength - 1 ).getOuterRange().end; newEndOffset = newDoc.internalList.getItemNode( newDoc.origInternalListLength - 1 ).getOuterRange().end; } else { oldEndOffset = listNodeRange.start; newEndOffset = newListNodeRange.start; } linearData = new ve.dm.ElementLinearData( doc.getStore(), newDoc.getData( new ve.Range( newListNodeRange.start, newEndOffset ), true ) ); listData = linearData.data .concat( doc.getData( new ve.Range( oldEndOffset, listNodeRange.end ), true ) ); } else { // newDoc is brand new, so use doc's internal list as a base listData = doc.getData( listNodeRange, true ); } for ( let i = 0, len = listMerge.newItemRanges.length; i < len; i++ ) { linearData = new ve.dm.ElementLinearData( doc.getStore(), newDoc.getData( listMerge.newItemRanges[ i ], true ) ); ve.batchPush( listData, linearData.data ); } const txBuilder = new ve.dm.TransactionBuilder(); let insertion; if ( offset <= listNodeRange.start ) { // offset is before listNodeRange // First replace the node, then the internal list // Fix up the node insertion insertion = doc.fixupInsertion( data.data, offset ); txBuilder.pushRetain( insertion.offset ); txBuilder.pushReplacement( doc, insertion.offset, insertion.remove, insertion.data, true ); txBuilder.pushRetain( listNodeRange.start - ( insertion.offset + insertion.remove ) ); txBuilder.pushReplacement( doc, listNodeRange.start, listNodeRange.end - listNodeRange.start, listData, true ); txBuilder.pushFinalRetain( doc, listNodeRange.end ); } else if ( offset >= listNodeRange.end ) { // offset is after listNodeRange // First replace the internal list, then the node // Fix up the node insertion insertion = doc.fixupInsertion( data.data, offset ); txBuilder.pushRetain( listNodeRange.start ); txBuilder.pushReplacement( doc, listNodeRange.start, listNodeRange.end - listNodeRange.start, listData, true ); txBuilder.pushRetain( insertion.offset - listNodeRange.end ); txBuilder.pushReplacement( doc, insertion.offset, insertion.remove, insertion.data, true ); txBuilder.pushFinalRetain( doc, insertion.offset + insertion.remove ); } else if ( offset >= listNodeRange.start && offset <= listNodeRange.end ) { // offset is within listNodeRange // Merge data into listData, then only replace the internal list // Find the internalItem we are inserting into let i = 0; // Find item node in doc let spliceItemRange; while ( ( spliceItemRange = doc.internalList.getItemNode( i ).getRange() ) && offset > spliceItemRange.end ) { i++; } let spliceListNodeRange; if ( newDoc.origInternalListLength !== null ) { // Get spliceItemRange from newDoc spliceItemRange = newDoc.internalList.getItemNode( i ).getRange(); spliceListNodeRange = newListNodeRange; } else { // Get spliceItemRange from doc; the while loop has already set it spliceListNodeRange = listNodeRange; } ve.batchSplice( listData, spliceItemRange.start - spliceListNodeRange.start, spliceItemRange.end - spliceItemRange.start, data.data ); txBuilder.pushRetain( listNodeRange.start ); txBuilder.pushReplacement( doc, listNodeRange.start, listNodeRange.end - listNodeRange.start, listData, true ); txBuilder.pushFinalRetain( doc, listNodeRange.end ); } return txBuilder.getTransaction(); }; /** * Generate a transaction that changes one or more attributes. * * @static * @param {ve.dm.Document} doc Document in pre-transaction state * @param {number} offset Offset of element * @param {Object.<string,any>} attr List of attribute key and value pairs, use undefined value * to remove an attribute * @return {ve.dm.Transaction} Transaction that changes an element * @throws {Error} Cannot set attributes to non-element data * @throws {Error} Cannot set attributes on closing element */ ve.dm.TransactionBuilder.static.newFromAttributeChanges = function ( doc, offset, attr ) { const txBuilder = new ve.dm.TransactionBuilder(), data = doc.getData(); // Verify element exists at offset if ( data[ offset ].type === undefined ) { throw new Error( 'Cannot set attributes to non-element data' ); } // Verify element is not a closing if ( data[ offset ].type.charAt( 0 ) === '/' ) { throw new Error( 'Cannot set attributes on closing element' ); } // Retain up to element txBuilder.pushRetain( offset ); // Change attributes txBuilder.pushAttributeChanges( attr, data[ offset ].attributes || {} ); // Retain to end of document txBuilder.pushFinalRetain( doc, offset ); return txBuilder.getTransaction(); }; /** * Generate a transaction that annotates content. * * @static * @param {ve.dm.Document} doc Document in pre-transaction state * @param {ve.Range} range Range to annotate * @param {string} method Annotation mode * - `set`: Adds annotation to all content in range * - `clear`: Removes instances of annotation from content in range * @param {ve.dm.Annotation} annotation Annotation to set or clear * @return {ve.dm.Transaction} Transaction that annotates content */ ve.dm.TransactionBuilder.static.newFromAnnotation = function ( doc, range, method, annotation ) { const clear = method === 'clear', runs = [], data = doc.data, hash = doc.getStore().hash( annotation ); let run = null, insideContentNode = false, ignoreChildrenDepth = 0; let i; /** * Return the array index of the annotation in the annotation array for the offset * * @param {number} offset Document offset * @return {number} Index in the annotation array (-1 if not present) */ function findAnnotation() { return data.getAnnotationHashesFromOffset( i ).lastIndexOf( hash ); } function startRun() { run = { start: i, end: null, data: null, spliceAt: clear ? findAnnotation() : null }; } function endRun() { run.end = i; if ( !clear ) { run.spliceAt = data.getCommonAnnotationArrayLength( new ve.Range( run.start, i ) ); } run.data = doc.getData( new ve.Range( run.start, run.end ) ); for ( let j = 0, jLen = run.data.length; j < jLen; j++ ) { let item = ve.copy( run.data[ j ] ); const anns = new ve.dm.AnnotationSet( doc.getStore(), ve.dm.ElementLinearData.static.getAnnotationHashesFromItem( item ) ); if ( clear ) { anns.remove( annotation ); } else { anns.add( annotation, run.spliceAt ); } item = ve.dm.ElementLinearData.static.replaceAnnotationHashesForItem( item, anns.getHashes() ); run.data[ j ] = item; } runs.push( run ); run = null; } let covered, arrayIndex; // Iterate over all data in range, finding "runs" to annotate for ( i = range.start; i < range.end; i++ ) { if ( data.isElementData( i ) && ve.dm.nodeFactory.shouldIgnoreChildren( data.getType( i ) ) ) { ignoreChildrenDepth += data.isOpenElementData( i ) ? 1 : -1; } const annotatable = !ignoreChildrenDepth && data.canTakeAnnotationAtOffset( i, annotation ); if ( !annotatable || ( insideContentNode && !data.isCloseElementData( i ) ) ) { // Structural element opening or closing, or entering a content node if ( run ) { endRun(); } continue; } if ( data.isCloseElementData( i ) ) { // Content closing, skip insideContentNode = false; continue; } if ( insideContentNode ) { continue; } // Else we're annotatable, not inside a content node, and have character or // content element opening if ( data.isElementData( i ) ) { insideContentNode = true; } if ( !clear ) { // Don't re-apply matching annotation covered = data.getAnnotationsFromOffset( i ).containsComparable( annotation ); } else { // Expect comparable annotations to be removed individually otherwise // we might try to remove more than one annotation per character, which // a single transaction can't do. arrayIndex = findAnnotation(); } if ( run && ( ( clear && arrayIndex !== run.spliceAt ) || ( !clear && covered ) ) ) { // Inside a run and: // - if clearing, annotation is absent entirely or at a new array index // - if setting, Matching annotation is present already endRun(); } if ( !run && ( ( clear && arrayIndex > -1 ) || ( !clear && !covered ) ) ) { // Not inside a run, and: // - if clearing, annotation is present // - if setting, annotation is not present startRun(); } } if ( run ) { endRun(); } const txBuilder = new ve.dm.TransactionBuilder(); let iLen; for ( i = 0, iLen = runs.length; i < iLen; i++ ) { run = runs[ i ]; txBuilder.pushRetain( run.start - ( i > 0 ? runs[ i - 1 ].end : 0 ) ); txBuilder.pushReplacement( doc, run.start, run.end - run.start, run.data, false ); } txBuilder.pushFinalRetain( doc, runs.length > 0 ? runs[ runs.length - 1 ].end : 0 ); return txBuilder.getTransaction(); }; /** * Generate a transaction that converts elements that can contain content. * * @static * @param {ve.dm.Document} doc Document in pre-transaction state * @param {ve.Range} range Range to convert * @param {string} type Symbolic name of element type to convert to * @param {Object} [attr] Attributes to initialize element with * @param {Object} [internal] Internal attributes to initialize element with * @return {ve.dm.Transaction} Transaction that converts content branches */ ve.dm.TransactionBuilder.static.newFromContentBranchConversion = function ( doc, range, type, attr, internal ) { const txBuilder = new ve.dm.TransactionBuilder(), selection = doc.selectNodes( range, 'leaves' ), opening = { type: type }, closing = { type: '/' + type }; let previousBranch, previousBranchOuterRange; // Add attributes to opening if needed if ( attr ) { if ( !ve.isEmptyObject( attr ) ) { opening.attributes = attr; } } else { // Set to empty object for comparison attr = {}; } if ( internal ) { if ( !ve.isEmptyObject( internal ) ) { opening.internal = internal; } } else { // Set to empty object for comparison internal = {}; } // Replace the wrappings of each content branch in the range for ( let i = 0, len = selection.length; i < len; i++ ) { const selected = selection[ i ]; // TODO: This duplicates the logic of ve.dm.SurfaceFragment#getSelectedLeafNodes // This could perhaps be a new mode in ve.Document#selectNodes? if ( !( len === 1 || !selected.range || !selected.range.isCollapsed() ) ) { continue; } const branch = selected.node.isContent() ? selected.node.getParent() : selected.node; if ( branch.canContainContent() ) { // Skip branches that are already of the target type and have matching attributes if ( branch.getType() === type && ve.compare( attr, branch.getAttributes() ) && ve.compare( internal, branch.element.internal || {} ) ) { continue; } const branchOuterRange = branch.getOuterRange(); // Don't convert the same branch twice if ( branch === previousBranch ) { continue; } // Retain up to this branch, considering where the previous one left off txBuilder.pushRetain( branchOuterRange.start - ( previousBranch ? previousBranchOuterRange.end : 0 ) ); if ( branch.getType() === type && ve.compare( internal, branch.element.internal || {} ) ) { // Same type and internal, different attributes, so we only need an attribute change txBuilder.pushAttributeChanges( attr, branch.getAttributes() ); // Retain the branch, including its opening and closing txBuilder.pushRetain( branch.getOuterLength() ); } else { // Types or internal differ, so we need to replace the opening and closing // Replace the opening txBuilder.pushReplacement( doc, branchOuterRange.start, 1, [ ve.copy( opening ) ] ); // Retain the contents txBuilder.pushRetain( branch.getLength() ); // Replace the closing txBuilder.pushReplacement( doc, branchOuterRange.end - 1, 1, [ ve.copy( closing ) ] ); } // Remember this branch and its range for next time previousBranch = branch; previousBranchOuterRange = branchOuterRange; } } // Retain until the end txBuilder.pushFinalRetain( doc, previousBranch ? previousBranchOuterRange.end : 0 ); return txBuilder.getTransaction(); }; /** * Generate a transaction that wraps, unwraps or replaces structure. * * The unwrap parameters are checked against the actual model data, and * an exception is thrown if the type fields don't match. This means you * can omit attributes from the unwrap parameters, those are automatically * picked up from the model data instead. * * NOTE: This function currently does not fix invalid parent/child relationships, so it will * happily convert paragraphs to listItems without wrapping them in a list if that's what you * ask it to do. We'll probably fix this later but for now the caller is responsible for giving * valid instructions. * * Changing a paragraph to a header: * Before: [ {type: 'paragraph'}, 'a', 'b', 'c', {type: '/paragraph'} ] * newFromWrap( new ve.Range( 1, 4 ), [ {type: 'paragraph'} ], [ {type: 'heading', level: 1 } ] ); * After: [ {type: 'heading', level: 1 }, 'a', 'b', 'c', {type: '/heading'} ] * * Changing a set of paragraphs to a list: * Before: [ {type: 'paragraph'}, 'a', {type: '/paragraph'}, {'type':'paragraph'}, 'b', {'type':'/paragraph'} ] * newFromWrap( new ve.Range( 0, 6 ), [], [ {type: 'list' } ], [], [ {type: 'listItem', attributes: {styles: ['bullet']}} ] ); * After: [ {type: 'list'}, {type: 'listItem', attributes: {styles: ['bullet']}}, {'type':'paragraph'} 'a', * {type: '/paragraph'}, {type: '/listItem'}, {type: 'listItem', attributes: {styles: ['bullet']}}, * {type: 'paragraph'}, 'b', {type: '/paragraph'}, {type: '/listItem'}, {type: '/list'} ] * * @param {ve.dm.Document} doc Document in pre-transaction state * @param {ve.Range} range Range to wrap/unwrap/replace around * @param {Array} unwrapOuter Opening elements to unwrap. These must be immediately *outside* the range * @param {Array} wrapOuter Opening elements to wrap around the range * @param {Array} unwrapEach Opening elements to unwrap from each top-level element in the range * @param {Array} wrapEach Opening elements to wrap around each top-level element in the range * @return {ve.dm.Transaction} */ ve.dm.TransactionBuilder.static.newFromWrap = function ( doc, range, unwrapOuter, wrapOuter, unwrapEach, wrapEach ) { const txBuilder = new ve.dm.TransactionBuilder(); let depth = 0; /** * Match items before/after an offset, skipping over MetaItems * * @param {string} direction Direction of match, backwards|forwards * @param {number} offset First boundary for the matching items * @param {Object[]} matchList List of objects with .type properties to compare * @param {string} matchName Description of the match, for error messages * @return {number} Other slice boundary for the matching items * @throws {Error} Unmatched item foo in matchName [(found bar)] */ function match( direction, offset, matchList, matchName ) { let start, stop, step; if ( direction === 'forwards' ) { start = 0; stop = matchList.length; step = 1; // Inclusive 'from' slice boundary offset--; } else { start = matchList.length - 1; stop = -1; step = -1; } for ( let j = start; j !== stop; j += step ) { let item; // Move to next item, skipping MetaItems while ( true ) { offset += step; item = doc.data.data[ offset ]; if ( !( ve.dm.LinearData.static.isElementData( item ) && ve.dm.nodeFactory.isMetaData( ve.dm.LinearData.static.getType( item ) ) ) ) { break; } } if ( !item || item.type !== matchList[ j ].type ) { throw new Error( 'Unmatched item ' + matchList[ j ].type + ' in ' + matchName + ' (found ' + ( item && item.type ) + ')' ); } } if ( direction === 'forwards' ) { // Exclusive 'to' slice boundary offset++; } return offset; } // Function to generate arrays of closing elements in reverse order function closingArray( openings ) { const closings = []; for ( let j = 0, jlen = openings.length; j < jlen; j++ ) { closings[ closings.length ] = { type: '/' + openings[ jlen - j - 1 ].type }; } return closings; } const closingUnwrapEach = closingArray( unwrapEach ); const closingWrapEach = closingArray( wrapEach ); // TODO: check for and fix nesting validity like fixupInsertion does // Verify the data before range.start matches unwrapOuter, and find where to retain up to let ptr = match( 'backwards', range.start, unwrapOuter, 'unwrapOuter' ); txBuilder.pushRetain( ptr ); // Replace wrapper (retaining any metadata) txBuilder.pushReplacement( doc, ptr, range.start - ptr, ve.copy( wrapOuter ) ); ptr = range.start; if ( wrapEach.length === 0 && unwrapEach.length === 0 ) { // There is no wrapEach/unwrapEach to be done, just retain // up to the end of the range txBuilder.pushRetain( range.end - range.start ); ptr = range.end; } else { // Visit each top-level child and wrap/unwrap it // TODO figure out if we should use the tree/node functions here // rather than iterating over offsets, it may or may not be faster let startOffset; for ( let i = range.start; i < range.end; i++ ) { if ( !doc.data.isElementData( i ) ) { continue; } if ( doc.data.isOpenElementData( i ) ) { depth++; if ( depth !== 1 || ve.dm.nodeFactory.isMetaData( ve.dm.LinearData.static.getType( doc.data.data[ i ] ) ) ) { continue; } // Retain any outstanding top level items (which must be MetaItems) txBuilder.pushRetain( i - ptr ); // This is the start of a top-level element ptr = match( 'forwards', i, unwrapEach, 'unwrapEach' ); // Replace wrapper (retaining any metadata) txBuilder.pushReplacement( doc, i, ptr - i, ve.copy( wrapEach ) ); startOffset = ptr; continue; } // Else this is a closing element depth--; if ( depth !== 0 || ve.dm.nodeFactory.isMetaData( ve.dm.LinearData.static.getType( doc.data.data[ i ] ) ) ) { continue; } // This is the end of a top-level element ptr = match( 'backwards', i + 1, closingUnwrapEach, 'closingUnwrapEach' ); // ptr is the range end of the unwrapped data (i.e. the offset after it) txBuilder.pushRetain( ptr - startOffset ); // Replace the closing elements (retaining any metadata) txBuilder.pushReplacement( doc, ptr, i + 1 - ptr, ve.copy( closingWrapEach ) ); ptr = i + 1; } } // Retain any outstanding top level items (which must be MetaItems) txBuilder.pushRetain( range.end - ptr ); ptr = match( 'forwards', range.end, closingArray( unwrapOuter ), 'unwrapOuter' ); txBuilder.pushReplacement( doc, range.end, ptr - range.end, closingArray( wrapOuter ) ); // Retain up to the end of the document txBuilder.pushFinalRetain( doc, ptr ); return txBuilder.getTransaction(); }; /* Methods */ /** * Get the transaction * * @return {ve.dm.Transaction} The transaction */ ve.dm.TransactionBuilder.prototype.getTransaction = function () { return this.transaction; }; /** * Add a final retain operation to finish off a transaction (internal helper). * * @private * @param {ve.dm.Document} doc The document in the state to which the transaction applies * @param {number} offset Final offset edited by the transaction up to this point. */ ve.dm.TransactionBuilder.prototype.pushFinalRetain = function ( doc, offset ) { if ( offset < doc.data.getLength() ) { this.pushRetain( doc.data.getLength() - offset ); } }; /** * Add a retain operation. * * @param {number} length Length of content data to retain * @throws {Error} Cannot retain backwards */ ve.dm.TransactionBuilder.prototype.pushRetain = function ( length ) { if ( length < 0 ) { throw new Error( 'Invalid retain length, cannot retain backwards:' + length ); } if ( length ) { const end = this.transaction.operations.length - 1; if ( this.transaction.operations.length && this.transaction.operations[ end ].type === 'retain' ) { this.transaction.operations[ end ].length += length; } else { this.transaction.pushRetainOp( length ); } } }; /** * Adds a replace op to remove the desired range and, where required, splices in retain ops * to prevent the deletion of undeletable nodes. * * @param {ve.dm.Document} doc The document in the state to which the transaction applies * @param {number} removeStart Offset to start removing from * @param {number} removeEnd Offset to remove to * @param {boolean} [removeMetadata=false] Remove metadata instead of collapsing it * @return {number} End offset of the removal */ ve.dm.TransactionBuilder.prototype.addSafeRemoveOps = function ( doc, removeStart, removeEnd, removeMetadata ) { let retainStart = removeStart, undeletableStackDepth = 0; // Iterate over removal range and use a stack counter to determine if // we are inside an undeletable node let queuedRetain; for ( let i = removeStart; i < removeEnd; i++ ) { if ( doc.data.isElementData( i ) && !ve.dm.nodeFactory.isNodeDeletable( doc.data.getType( i ) ) ) { if ( !doc.data.isCloseElementData( i ) ) { if ( undeletableStackDepth === 0 ) { if ( queuedRetain ) { this.pushRetain( queuedRetain ); } this.pushReplacement( doc, removeStart, i - removeStart, [], removeMetadata ); retainStart = i; } undeletableStackDepth++; } else { undeletableStackDepth--; if ( undeletableStackDepth === 0 ) { queuedRetain = i + 1 - retainStart; removeStart = i + 1; } } } } if ( removeEnd - removeStart ) { if ( queuedRetain ) { this.pushRetain( queuedRetain ); } this.pushReplacement( doc, removeStart, removeEnd - removeStart, [], removeMetadata ); retainStart = removeEnd; } return retainStart; }; /** * Add a replace operation (internal helper). * * @private * @param {Array} remove Data removed. * @param {Array} insert Data to insert. * @param {number} [insertedDataOffset] Inserted data offset * @param {number} [insertedDataLength] Inserted data length */ ve.dm.TransactionBuilder.prototype.pushReplaceInternal = function ( remove, insert, insertedDataOffset, insertedDataLength ) { if ( remove.length === 0 && insert.length === 0 ) { return; // No-op } this.transaction.pushReplaceOp( remove, insert, insertedDataOffset, insertedDataLength ); }; /** * Add a replace operation * * If metadata is collapsed instead of removed, it will be shifted backwards if necessary to * reach a legal position for metadata in the new document structure. * * @param {ve.dm.Document} doc The document in the state to which the transaction applies * @param {number} offset Offset to start at * @param {number} removeLength Number of data items to remove * @param {Array} insert Data to insert * @param {boolean} removeMetadata Remove metadata instead of collapsing it * @param {number} [insertedDataOffset] Offset of the originally inserted data in the resulting operation data * @param {number} [insertedDataLength] Length of the originally inserted data in the resulting operation data */ ve.dm.TransactionBuilder.prototype.pushReplacement = function ( doc, offset, removeLength, insert, removeMetadata, insertedDataOffset, insertedDataLength ) { let remove = doc.getData( new ve.Range( offset, offset + removeLength ) ); const collapse = removeMetadata ? [] : remove.filter( ( item ) => { const type = ve.dm.LinearData.static.isElementData( item ) && ve.dm.LinearData.static.getType( item ); return type && ve.dm.nodeFactory.isMetaData( type ) && !ve.dm.nodeFactory.isRemovableMetaData( type ); } ); if ( removeLength === collapse.length && insert.length === 0 ) { // Push retain instead of no-op replace this.pushRetain( collapse.length ); return; } if ( collapse.length > 0 ) { // Push collapsed metadata, moved backwards if this position has become invalid // for metadata this.pushMeta( doc, offset, collapse ); } // Merge with previous replace, if any (which may have come from the pushMeta above) const lastOp = this.transaction.operations[ this.transaction.operations.length - 1 ]; if ( lastOp && lastOp.type === 'replace' && !lastOp.insertedDataOffset && !insertedDataOffset ) { this.transaction.operations.pop(); remove = lastOp.remove.concat( remove ); insert = lastOp.insert.concat( insert ); } const op = { type: 'replace', remove: remove, insert: insert }; if ( insertedDataOffset !== undefined ) { op.insertedDataOffset = insertedDataOffset; } if ( insertedDataLength !== undefined ) { op.insertedDataLength = insertedDataLength; } this.transaction.operations.push( op ); }; /** * Add an element attribute change operation. * * @param {string} key Name of attribute to change * @param {any} from Value change attribute from, or undefined if not previously set * @param {any} to Value to change attribute to, or undefined to remove */ ve.dm.TransactionBuilder.prototype.pushReplaceElementAttribute = function ( key, from, to ) { this.transaction.pushAttributeOp( key, from, to ); }; /** * Add a series of element attribute change operations. * * @param {Object} changes Object mapping attribute names to new values * @param {Object} oldAttrs Object mapping attribute names to old values */ ve.dm.TransactionBuilder.prototype.pushAttributeChanges = function ( changes, oldAttrs ) { for ( const key in changes ) { if ( oldAttrs[ key ] !== changes[ key ] ) { this.pushReplaceElementAttribute( key, ve.copy( oldAttrs[ key ] ), ve.copy( changes[ key ] ) ); } } }; /** * Internal helper method for newFromInsertion and newFromReplacement. * Adds an insertion to an existing transaction object. * * @private * @param {ve.dm.Document} doc The document in the state to which the transaction applies * @param {number} currentOffset Offset up to which the transaction has gone already * @param {number} insertOffset Offset to insert at * @param {Array} data Linear model data to insert * @return {number} End offset of the insertion */ ve.dm.TransactionBuilder.prototype.pushInsertion = function ( doc, currentOffset, insertOffset, data ) { // Fix up the insertion const insertion = doc.fixupInsertion( data, insertOffset ); // Retain up to insertion point, if needed this.pushRetain( insertion.offset - currentOffset ); // Insert data this.pushReplacement( doc, insertion.offset, insertion.remove, insertion.data, false, insertion.insertedDataOffset, insertion.insertedDataLength ); return insertion.offset + insertion.remove; }; /** * Internal helper method for newFromRemoval and newFromReplacement. * Adds a removal to an existing transaction object. * * @private * @param {ve.dm.Document} doc The document in the state to which the transaction applies * @param {number} currentOffset Offset up to which the transaction has gone already * @param {ve.Range} range Range to remove * @param {boolean} [removeMetadata=false] Remove metadata instead of collapsing it * @return {number} End offset of the removal */ ve.dm.TransactionBuilder.prototype.pushRemoval = function ( doc, currentOffset, range, removeMetadata ) { let offset = currentOffset, removeStart = null, removeEnd = null; // Validate range if ( range.isCollapsed() ) { // Empty range, nothing to remove this.pushRetain( range.start - currentOffset ); return range.start; } // Select nodes and validate selection const selection = doc.selectNodes( range, 'covered' ); if ( selection.length === 0 ) { // Empty selection? Something is wrong! throw new Error( 'Invalid range, cannot remove from ' + range.start + ' to ' + range.end ); } const first = selection[ 0 ]; const last = selection[ selection.length - 1 ]; // If the first and last node are mergeable, merge them if ( first.node.canBeMergedWith( last.node ) ) { if ( !first.range && !last.range ) { // First and last node are both completely covered, remove them removeStart = first.nodeOuterRange.start; removeEnd = last.nodeOuterRange.end; } else { // Either the first node or the last node is partially covered, so remove // the selected content. The other node might be fully covered, in which case // we remove its contents (nodeRange). For fully covered content nodes, we must // remove the entire node (nodeOuterRange). removeStart = ( first.range || ( first.node.isContent() ? first.nodeOuterRange : first.nodeRange ) ).start; removeEnd = ( last.range || ( last.node.isContent() ? last.nodeOuterRange : last.nodeRange ) ).end; } this.pushRetain( removeStart - currentOffset ); removeEnd = this.addSafeRemoveOps( doc, removeStart, removeEnd, removeMetadata ); // All done return removeEnd; } // The selection wasn't mergeable, so remove nodes that are completely covered, and strip // nodes that aren't for ( let i = 0; i < selection.length; i++ ) { let nodeStart, nodeEnd; if ( !selection[ i ].range ) { // Entire node is covered, remove it nodeStart = selection[ i ].nodeOuterRange.start; nodeEnd = selection[ i ].nodeOuterRange.end; } else { // Part of the node is covered, remove that range nodeStart = selection[ i ].range.start; nodeEnd = selection[ i ].range.end; } // Merge contiguous removals. Only apply a removal when a gap appears, or at the // end of the loop if ( removeEnd === null ) { // First removal removeStart = nodeStart; removeEnd = nodeEnd; } else if ( removeEnd === nodeStart ) { // Merge this removal into the previous one removeEnd = nodeEnd; } else { // There is a gap between the previous removal and this one // Push the previous removal first this.pushRetain( removeStart - offset ); offset = this.addSafeRemoveOps( doc, removeStart, removeEnd, removeMetadata ); // Now start this removal removeStart = nodeStart; removeEnd = nodeEnd; } } // Apply the last removal, if any if ( removeEnd !== null ) { this.pushRetain( removeStart - offset ); offset = this.addSafeRemoveOps( doc, removeStart, removeEnd, removeMetadata ); } return offset; }; /** * Insert meta items, as near to the current transaction end as is legal * * @param {ve.dm.Document} doc Document in pre-transaction state * @param {number} offset Offset of the end of the current (partial) operations list * @param {Array} metaItems linear data containing just meta items */ ve.dm.TransactionBuilder.prototype.pushMeta = function ( doc, offset, metaItems ) { const ops = this.transaction.operations; let position = null, relDepth = 0; if ( ops.length === 0 ) { ops.push( { type: 'replace', remove: [], insert: metaItems } ); return; } // Start at the current end of the transaction position = { opIndex: ops.length - 1, itemIndex: ops[ ops.length - 1 ].length }; let op; // Walk backwards over the linear data as it will be after the transaction is // applied, tracking depth, and disregarding tags deeper than the depth at offset. // - If we hit a structural open tag with restricted childNodeTypes (like list) then // move position to just before the tag, and keep walking // - If we hit a structural open tag with unrestricted childNodeTypes, or the document // start, then position is fine: insert metaItems there and return findPositionLoop: for ( let i = ops.length - 1; i >= 0; i-- ) { op = ops[ i ]; let items; if ( op.type === 'replace' ) { items = op.insert; offset -= op.remove.length; } else if ( op.type === 'retain' ) { items = doc.getData( new ve.Range( offset - op.length, offset ) ); offset -= op.length; } else { // attribute/annotate: do nothing continue; } for ( let j = items.length - 1; j >= 0; j-- ) { const type = items[ j ].type; if ( !type || typeof type !== 'string' ) { continue; } if ( type.charAt( 0 ) === '/' ) { // Close element, so going backwards we're getting deeper relDepth++; continue; } // Else open element relDepth--; if ( relDepth >= 0 ) { continue; // TODO: This could be a long walk. Do we want to infer stuff // from element types we see at relDepth 0? For instance, if // we see a structural node at the same depth as position, with // unrestricted parentNodeTypes, could we perhaps infer that // position is a legal place for metadata without walking the // whole 874 miles to the start of the document? // // This is not an immediate priority because for text // replacement during typing (the main performance-critical use // case) we only have to walk back to the start of the CBN, // which is unlikely to be far. } // We won't let relDepth go below -1 (see below), so this element is // the parent of position if ( !ve.dm.nodeFactory.canNodeContainContent( type ) && !ve.dm.nodeFactory.getChildNodeTypes( type ) ) { // This is a legal parent for metadata break findPositionLoop; } // Else not a legal parent for metadata: move position up and continue position = { opIndex: i, itemIndex: j }; relDepth = 0; } } op = ops[ position.opIndex ]; if ( op.type === 'replace' ) { ve.batchSplice( op.insert, position.itemIndex, 0, metaItems ); return; } // Else it's a retain op (it cannot be attribute/annotate because we skip those above) if ( position.itemIndex > 0 ) { ops.splice( position.opIndex, 0, { type: 'retain', length: position.itemIndex } ); position.opIndex++; } ops.splice( position.opIndex, 0, { type: 'replace', remove: [], insert: metaItems } ); op.length -= position.itemIndex; if ( op.length === 0 ) { ops.splice( position.opIndex + 1, 1 ); } // TODO: normalize resulting consecutive replace ops? };
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка