Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/VisualEditor/lib/ve/src/ui/elements/ve.ui.DiffElement.js
Ðазад
/*! * VisualEditor UserInterface DiffElement class. * * @copyright See AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * Creates a ve.ui.DiffElement object. * * @class * @extends OO.ui.Element * * @constructor * @param {ve.dm.VisualDiff} [visualDiff] Diff to visualize * @param {Object} [config] */ ve.ui.DiffElement = function VeUiDiffElement( visualDiff, config ) { const diff = visualDiff.diff; // Parent constructor ve.ui.DiffElement.super.call( this, config ); this.elementId = 0; this.$overlays = $( '<div>' ).addClass( 've-ui-diffElement-overlays' ); this.$content = $( '<div>' ).addClass( 've-ui-diffElement-content' ); this.$messages = $( '<div>' ).addClass( 've-ui-diffElement-messages' ); this.$document = $( '<div>' ).addClass( 've-ui-diffElement-document' ); this.$sidebar = $( '<div>' ).addClass( 've-ui-diffElement-sidebar' ); this.descriptions = new ve.ui.ChangeDescriptionsSelectWidget(); this.descriptions.connect( this, { highlight: 'onDescriptionsHighlight' } ); // Set to an empty array before a series of diff computations to collect descriptions. // Set back to null after collecting values. this.descriptionItemsStack = null; this.$document.on( { mousemove: this.onDocumentMouseMove.bind( this ) } ); this.renderDiff( diff.docDiff, diff.internalListDiff, diff.metaListDiff, visualDiff.newDoc.getHtmlDocument() ); if ( visualDiff.timedOut ) { const warning = new OO.ui.MessageWidget( { type: 'warning', classes: [ 've-ui-diffElement-warning' ], label: ve.msg( 'visualeditor-diff-timed-out' ) } ); this.$messages.append( warning.$element ); } // DOM this.$element .append( this.$messages, this.$content.append( this.$document, this.$overlays ), this.$sidebar.append( this.descriptions.$element ) ) .addClass( 've-ui-diffElement' ); }; /* Inheritance */ OO.inheritClass( ve.ui.DiffElement, OO.ui.Element ); /* Static methods */ /** * Compare attribute sets between two elements * * @param {Object} oldAttributes Old attributes * @param {Object} newAttributes New attributes * @return {Object} Keyed set of attributes */ ve.ui.DiffElement.static.compareAttributes = function ( oldAttributes, newAttributes ) { function compareKeys( a, b ) { if ( typeof a === 'object' && typeof b === 'object' ) { return ve.compare( a, b ); } else { return a === b; } } const attributeChanges = {}; for ( const key in oldAttributes ) { if ( !compareKeys( oldAttributes[ key ], newAttributes[ key ] ) ) { attributeChanges[ key ] = { from: oldAttributes[ key ], to: newAttributes[ key ] }; } } for ( const key in newAttributes ) { if ( !Object.prototype.hasOwnProperty.call( oldAttributes, key ) && newAttributes[ key ] !== undefined ) { attributeChanges[ key ] = { from: oldAttributes[ key ], to: newAttributes[ key ] }; } } return attributeChanges; }; /** * Get the original linear data from a node * * @param {ve.dm.Node} node Node * @return {Array} Linear data */ ve.ui.DiffElement.static.getDataFromNode = function ( node ) { const doc = node.getRoot().getDocument(); return doc.getData( node.getOuterRange() ); }; /* Methods */ /** * Get a diff element in the document from its elementId * * @param {number} elementId ID * @return {jQuery} Element */ ve.ui.DiffElement.prototype.getDiffElementById = function ( elementId ) { return this.$document.find( '[data-diff-id=' + elementId + ']' ); }; /** * Handle description item hightlight events * * @param {OO.ui.OptionWidget} item Description item */ ve.ui.DiffElement.prototype.onDescriptionsHighlight = function ( item ) { if ( this.lastItem ) { this.getDiffElementById( this.lastItem.getData() ).css( 'outline', '' ); this.$overlays.empty(); } if ( item ) { const overlayRect = this.$overlays[ 0 ].getBoundingClientRect(); const elementRects = ve.ce.FocusableNode.static.getRectsForElement( this.getDiffElementById( item.getData() ), overlayRect ).rects; for ( let i = 0, l = elementRects.length; i < l; i++ ) { this.$overlays.append( $( '<div>' ).addClass( 've-ui-diffElement-highlight' ).css( { top: elementRects[ i ].top, left: elementRects[ i ].left, width: elementRects[ i ].width, height: elementRects[ i ].height } ) ); } this.lastItem = item; } }; /** * Handle document mouse move events * * @param {jQuery.Event} e Mouse move event */ ve.ui.DiffElement.prototype.onDocumentMouseMove = function ( e ) { const elementId = $( e.target ).closest( '[data-diff-id]' ).attr( 'data-diff-id' ); if ( elementId !== undefined ) { this.descriptions.highlightItem( this.descriptions.findItemFromData( +elementId ) ); } else { this.descriptions.highlightItem(); } }; /** * Reposition the description items so they are not above their position in the document */ ve.ui.DiffElement.prototype.positionDescriptions = function () { this.descriptions.getItems().forEach( ( item ) => { item.$element.css( 'margin-top', '' ); const itemRect = item.$element[ 0 ].getBoundingClientRect(); const $element = this.getDiffElementById( item.getData() ); if ( !$element.length ) { // Changed element isn't visible - probably shouldn't happen return; } const elementRect = ve.ce.FocusableNode.static.getRectsForElement( $element ).boundingRect; // elementRect can currently be null for meta items, e.g. <link> if ( elementRect && elementRect.top > itemRect.top ) { item.$element.css( 'margin-top', elementRect.top - itemRect.top - 5 ); } } ); this.$document.css( 'min-height', this.$sidebar.height() ); }; /** * Process a diff queue, skipping over sequential nodes with no changes * * @param {Array[]} queue Diff queue * @return {Array.<Array|null>} */ ve.ui.DiffElement.prototype.processQueue = function processQueue( queue ) { let hasChanges = false, lastItemSpacer = false, needsSpacer = false, headingContext = null, headingContextSpacer = false; const processedQueue = []; function isUnchanged( item ) { return !item || ( item[ 2 ] === 'none' && !item[ 3 ] ); } function addSpacer() { processedQueue.push( null ); lastItemSpacer = true; } function addItem( item ) { processedQueue.push( item ); lastItemSpacer = false; } function isHeading( item ) { switch ( item[ 0 ] ) { case 'getNodeData': case 'getNodeElements': return item[ 1 ] instanceof ve.dm.HeadingNode; case 'getChangedNodeData': case 'getChangedNodeElements': return item[ 3 ] instanceof ve.dm.HeadingNode; } } for ( let k = 0, klen = queue.length; k < klen; k++ ) { if ( !isUnchanged( queue[ k - 1 ] ) || !isUnchanged( queue[ k ] ) || !isUnchanged( queue[ k + 1 ] ) ) { hasChanges = true; if ( headingContext ) { // Don't render headingContext if current or next node is a heading if ( !isHeading( queue[ k ] ) && !isHeading( queue[ k + 1 ] ) ) { if ( headingContextSpacer ) { addSpacer(); } addItem( headingContext ); } else if ( isHeading( queue[ k + 1 ] ) ) { // Skipping the context header becuase the next node is a heading // so reinstate the spacer. needsSpacer = true; } headingContext = null; } if ( needsSpacer && !lastItemSpacer ) { addSpacer(); needsSpacer = false; } addItem( queue[ k ] ); if ( isHeading( queue[ k ] ) ) { // Heading was rendered, no need to show it as context headingContext = null; } } else { // Heading skipped, maybe show as context later if ( isHeading( queue[ k ] ) ) { headingContext = isUnchanged( queue[ k ] ) ? queue[ k ] : null; headingContextSpacer = needsSpacer; needsSpacer = false; } else { needsSpacer = true; } } } // Trailing spacer if ( hasChanges && needsSpacer && !lastItemSpacer ) { addSpacer(); } return processedQueue; }; /** * @param {Array.<Array|null>} queue Diff queue * @param {HTMLElement} parentNode Parent node to render to * @param {HTMLElement} spacerNode Spacer node template */ ve.ui.DiffElement.prototype.renderQueue = function ( queue, parentNode, spacerNode ) { queue.forEach( ( item ) => { if ( item ) { const elements = this[ item[ 0 ] ].apply( this, item.slice( 1 ) ); while ( elements.length ) { parentNode.appendChild( parentNode.ownerDocument.adoptNode( elements[ 0 ] ) ); elements.shift(); } } else { parentNode.appendChild( parentNode.ownerDocument.adoptNode( spacerNode.cloneNode( true ) ) ); } } ); }; /** * Render the diff * * @param {Object} diff Object describing the diff * @param {Object} internalListDiff Object describing the diff of the internal list * @param {Object} metaListDiff Object describing the diff of the meta list * @param {HTMLDocument} newHtmlDocument HTML document of context, for resolving attributes */ ve.ui.DiffElement.prototype.renderDiff = function ( diff, internalListDiff, metaListDiff, newHtmlDocument ) { const documentNode = this.$document[ 0 ], diffQueue = []; let internalListDiffQueue = []; const documentSpacerNode = document.createElement( 'div' ); documentSpacerNode.setAttribute( 'class', 've-ui-diffElement-spacer' ); documentSpacerNode.appendChild( document.createTextNode( '⋮' ) ); const internalListSpacerNode = document.createElement( 'li' ); internalListSpacerNode.setAttribute( 'class', 've-ui-diffElement-internalListSpacer' ); internalListSpacerNode.appendChild( documentSpacerNode.cloneNode( true ) ); const referencesListDiffs = {}; Object.keys( internalListDiff.groups ).forEach( ( group ) => { const referencesListContainer = document.createElement( 'ol' ); const internalListGroup = internalListDiff.groups[ group ]; this.iterateDiff( internalListGroup, { insert: function ( newNode, newIndex ) { internalListDiffQueue.push( [ 'getInternalListNodeElements', newNode, 'insert', null, newIndex ] ); }, remove: function ( oldNode, oldIndex ) { internalListDiffQueue.push( [ 'getInternalListNodeElements', oldNode, 'remove', null, oldIndex ] ); }, move: function ( newNode, move, newIndex ) { internalListDiffQueue.push( [ 'getInternalListNodeElements', newNode, 'none', move, newIndex ] ); }, changed: function ( nodeDiff, oldNode, newNode, move, oldIndex, newIndex ) { internalListDiffQueue.push( [ 'getInternalListChangedNodeElements', nodeDiff, oldNode, newNode, move, newIndex ] ); } } ); this.descriptionItemsStack = []; this.renderQueue( this.processQueue( internalListDiffQueue ), referencesListContainer, internalListSpacerNode ); referencesListDiffs[ group ] = { element: referencesListContainer, action: internalListGroup.changes ? 'change' : 'none', descriptionItemsStack: this.descriptionItemsStack, shown: false }; this.descriptionItemsStack = null; internalListDiffQueue = []; } ); this.descriptionItemsStack = []; let referencesListDiff; function handleRefList( node, move ) { if ( node.type === 'mwReferencesList' && ( referencesListDiff = referencesListDiffs[ node.element.attributes.listGroup ] ) ) { // New node is a references list node. If a reference has // changed, the references list nodes appear unchanged, // because of how the internal list works. However, we // already have the HTML for the diffed references list, // (which contains details of changes if there are any) so // just get that. diffQueue.push( [ 'getRefListNodeElements', referencesListDiff.element, referencesListDiff.action, move, referencesListDiff.descriptionItemsStack ] ); referencesListDiff.shown = true; return true; } return false; } this.iterateDiff( diff, { insert: function ( newNode ) { if ( !handleRefList( newNode, null ) ) { diffQueue.push( [ 'getNodeElements', newNode, 'insert', null ] ); } }, remove: function ( oldNode ) { if ( !handleRefList( oldNode, null ) ) { diffQueue.push( [ 'getNodeElements', oldNode, 'remove', null ] ); } }, move: function ( newNode, move ) { diffQueue.push( [ 'getNodeElements', newNode, 'none', move ] ); }, preChanged: function ( oldNode, newNode, move ) { return handleRefList( newNode, move ); }, changed: function ( nodeDiff, oldNode, newNode, move ) { diffQueue.push( [ 'getChangedNodeElements', nodeDiff, oldNode, newNode, move ] ); } } ); // Show any ref list diffs that weren't picked up by the main diff loop above, // e.g. during a section diff. Object.keys( referencesListDiffs ).forEach( ( group ) => { referencesListDiff = referencesListDiffs[ group ]; if ( !referencesListDiff.shown ) { diffQueue.push( [ 'getRefListNodeElements', referencesListDiff.element, referencesListDiff.action, null, referencesListDiff.descriptionItemsStack ] ); } } ); this.renderQueue( this.processQueue( diffQueue ), documentNode, documentSpacerNode ); this.renderMetaListDiff( metaListDiff, documentNode, documentSpacerNode ); this.descriptions.addItems( this.descriptionItemsStack ); this.descriptionItemsStack = null; ve.resolveAttributes( documentNode, newHtmlDocument, ve.dm.Converter.static.computedAttributes ); if ( !documentNode.children.length ) { const noChanges = document.createElement( 'div' ); noChanges.setAttribute( 'class', 've-ui-diffElement-no-changes' ); noChanges.appendChild( document.createTextNode( ve.msg( 'visualeditor-diff-no-changes' ) ) ); documentNode.innerHTML = ''; documentNode.appendChild( noChanges ); } this.$element .toggleClass( 've-ui-diffElement-hasDescriptions', !this.descriptions.isEmpty() ); }; ve.ui.DiffElement.prototype.renderMetaListDiff = function ( metaListDiff, documentNode, documentSpacerNode ) { Object.keys( metaListDiff ).forEach( ( group ) => { const handler = ve.ui.metaListDiffRegistry.lookup( group ); if ( handler ) { const diffQueue = []; this.iterateDiff( metaListDiff[ group ], { insert: ( newNode ) => { diffQueue.push( [ 'getNodeElements', newNode, 'insert', null ] ); }, remove: ( oldNode ) => { diffQueue.push( [ 'getNodeElements', oldNode, 'remove', null ] ); }, move: ( newNode, move ) => { diffQueue.push( [ 'getNodeElements', newNode, 'none', move ] ); }, changed: ( nodeDiff, oldNode, newNode, move ) => { diffQueue.push( [ 'getChangedNodeElements', nodeDiff, oldNode, newNode, move ] ); } } ); handler( this, diffQueue, documentNode, documentSpacerNode ); } } ); }; /** * Get the HTML for the diff of a removed, inserted, or unchanged-but-moved node. * * @param {ve.dm.Node} node The node being diffed. Will be from the old * document if it has been removed, or the new document if it has been inserted * or moved * @param {string} action 'remove', 'insert' or, if moved, 'none' * @param {string|null} move 'up' or 'down' if the node has moved * @return {HTMLElement[]} Elements (not owned by window.document) */ ve.ui.DiffElement.prototype.getNodeElements = function ( node, action, move ) { const nodeData = this.getNodeData( node, action, move ); return this.wrapNodeData( node.getRoot().getDocument(), nodeData ); }; /** * Get the DOM from linear data and wrap it for the diff. * * @param {ve.dm.Document} nodeDoc Node's document model * @param {Array} nodeData Linear data for the diff * @return {HTMLElement[]} Elements (not owned by window.document) */ ve.ui.DiffElement.prototype.wrapNodeData = function ( nodeDoc, nodeData ) { const documentSlice = nodeDoc.cloneWithData( nodeData ); documentSlice.getStore().merge( nodeDoc.getStore() ); const nodeElements = ve.dm.converter.getDomFromModel( documentSlice, ve.dm.Converter.static.PREVIEW_MODE ).body; // Convert NodeList to real array return Array.prototype.slice.call( nodeElements.childNodes ); }; /** * Get the linear data for the diff of a removed, inserted, or * unchanged-but-moved node. * * @param {ve.dm.Node} node * @param {string} action 'remove', 'insert' or, if moved, 'none' * @param {string|null} move 'up' or 'down' if the node has moved * @return {Array} Linear Data */ ve.ui.DiffElement.prototype.getNodeData = function ( node, action, move ) { // Get the linear model for the node const nodeData = this.constructor.static.getDataFromNode( node ); // Add the classes to the outer element this.addAttributesToElement( nodeData, 0, { 'data-diff-action': action } ); this.markMove( move, nodeData ); return nodeData; }; /** * Get the HTML for the diff of a node that has been changed. * * @param {Object} diff Object describing the diff * @param {ve.dm.Node} oldNode Node from the old document * @param {ve.dm.Node} newNode Corresponding node from the new document * @param {string|null} move 'up' or 'down' if the node has moved * @return {HTMLElement[]} Elements (not owned by window.document) */ ve.ui.DiffElement.prototype.getChangedNodeElements = function ( diff, oldNode, newNode, move ) { const nodeData = this.getChangedNodeData( diff, oldNode, newNode, move ); return this.wrapNodeData( newNode.getRoot().getDocument(), nodeData ); }; /** * Get the linear data for the diff of a node that has been changed. * * @param {Object} diff Object describing the diff * @param {ve.dm.Node} oldNode Node from the old document * @param {ve.dm.Node} newNode Corresponding node from the new document * @param {string|null} move 'up' or 'down' if the node has moved * @param {boolean} [noTreeDiff] Don't perform a tree diff of the nodes (used internally to avoid recursion) * @return {Array|boolean} Linear data for the diff, or false */ ve.ui.DiffElement.prototype.getChangedNodeData = function ( diff, oldNode, newNode, move, noTreeDiff ) { let nodeData; // Choose the appropriate method for the type of node if ( newNode.isDiffedAsLeaf() ) { nodeData = this.getChangedLeafNodeData( newNode, diff, move ); } else if ( newNode.isDiffedAsList() ) { nodeData = this.getChangedListNodeData( newNode, diff ); } else if ( newNode.isDiffedAsDocument() ) { nodeData = this.getChangedDocListData( newNode, diff ); } else if ( !noTreeDiff ) { nodeData = this.getChangedTreeNodeData( oldNode, newNode, diff ); } else { return false; } this.markMove( move, nodeData ); return nodeData; }; /** * Get the linear data for the diff of a leaf-like node that has been changed. * * @param {ve.dm.Node} newNode Corresponding node from the new document * @param {Object} diff Object describing the diff * @return {Array} Linear data for the diff */ ve.ui.DiffElement.prototype.getChangedLeafNodeData = function ( newNode, diff ) { const nodeData = this.constructor.static.getDataFromNode( newNode ), linearDiff = diff.linearDiff, attributeChange = diff.attributeChange; if ( linearDiff ) { // If there is a content change, splice it in const annotatedData = this.annotateNode( linearDiff, newNode ); ve.batchSplice( nodeData, 1, newNode.length, annotatedData ); } if ( attributeChange ) { // If there is no content change, just add change class this.addAttributesToElement( nodeData, 0, { 'data-diff-action': 'structural-change' } ); const item = this.compareNodeAttributes( nodeData, 0, attributeChange ); if ( item ) { this.descriptionItemsStack.push( item ); } } return nodeData; }; /** * Append list item * * @private * @param {Array} diffData * @param {number} insertIndex * @param {ve.dm.ListNode} listNode * @param {Array} listNodeData List node opening * @param {Array} listItemData * @param {number} depthChange * @return {number} */ ve.ui.DiffElement.prototype.appendListItem = function ( diffData, insertIndex, listNode, listNodeData, listItemData, depthChange ) { if ( depthChange === 0 ) { // Current list item belongs to the same list as the previous list item ve.batchSplice( diffData, insertIndex, 0, listItemData ); insertIndex += listItemData.length; } else if ( depthChange > 0 ) { // Begin a new nested list, with this node's ancestor nodes const linearData = []; linearData.unshift( listNodeData[ 0 ] ); let k, klen; const doc = listNode.getRoot().getDocument(); // Nested list may be nested by multiple levels for ( k = 0, klen = depthChange - 1; k < klen; k++ ) { const listItemNode = listNode.parent; linearData.unshift( doc.data.data[ listItemNode.getOuterRange().from ] ); listNode = listItemNode.parent; linearData.unshift( doc.data.data[ listNode.getOuterRange().from ] ); } // Splice in the content, and splice that into the diff data ve.batchSplice( linearData, linearData.length, 0, listItemData ); for ( k = 0, klen = depthChange - 1; k < klen; k++ ) { linearData.push( { type: '/list' } ); linearData.push( { type: '/listItem' } ); } linearData.push( { type: '/list' } ); // Splice into previous list item insertIndex -= 1; ve.batchSplice( diffData, insertIndex, 0, linearData ); // Adjust the insertIndex to be at the end of this content insertIndex += 2 * ( depthChange - 1 ) + 1 + listItemData.length; } else if ( depthChange < 0 ) { // Skip over close elements to get out of nested list(s) insertIndex += 2 * -depthChange; // Splice in the data and adjust the insert index ve.batchSplice( diffData, insertIndex, 0, listItemData ); insertIndex += listItemData.length; } return insertIndex; }; /** * Get the linear data for a document-like node that has been changed * * @param {ve.dm.Node} newDoclistNode Node from new document * @param {Object} diff Object describing the diff * @param {boolean} neverProcess Never process the diffQueue (always show the whole document) * @return {Array} Linear data for the diff */ ve.ui.DiffElement.prototype.getChangedDocListData = function ( newDoclistNode, diff, neverProcess ) { const diffData = []; let diffQueue = []; const spacerData = [ { type: 'div' }, '⋮', { type: '/div' } ]; this.addAttributesToElement( spacerData, 0, { class: 've-ui-diffElement-spacer' } ); this.iterateDiff( diff, { insert: ( newNode ) => { diffQueue.push( [ 'getNodeData', newNode, 'insert', null ] ); }, remove: ( oldNode ) => { diffQueue.push( [ 'getNodeData', oldNode, 'remove', null ] ); }, move: ( newNode, move ) => { diffQueue.push( [ 'getNodeData', newNode, 'none', move ] ); }, changed: ( nodeDiff, oldNode, newNode, move ) => { diffQueue.push( [ 'getChangedNodeData', nodeDiff, oldNode, newNode, move ] ); } } ); let hasAttributeChanges = false; const newDoclistNodeData = this.constructor.static.getDataFromNode( newDoclistNode ); if ( diff.attributeChange ) { const item = this.compareNodeAttributes( newDoclistNodeData, 0, diff.attributeChange ); if ( item ) { this.descriptionItemsStack.push( item ); hasAttributeChanges = true; } } // When the doc cotainer has attribute changes, show the whole node. // Otherwise use processQueue to filter out unchanged context if ( !neverProcess && !hasAttributeChanges ) { diffQueue = this.processQueue( diffQueue ); } diffQueue.forEach( ( diffItem ) => { if ( diffItem ) { ve.batchPush( diffData, this[ diffItem[ 0 ] ].apply( this, diffItem.slice( 1 ) ) ); } else { ve.batchPush( diffData, spacerData.slice() ); } } ); // Wrap in newDocListNode diffData.unshift( newDoclistNodeData[ 0 ] ); diffData.push( newDoclistNodeData[ newDoclistNodeData.length - 1 ] ); return diffData; }; /** * Iterate over a diff object and run more meaningful callbacks * * @param {Object|Array} diff Diff object, or array (InternalListDiff) * @param {Object} callbacks Callbacks * @param {Function} callbacks.insert Node inserted, arguments: * {ve.dm.Node} newNode * {number} newIndex * @param {Function} callbacks.remove Node removed, arguments: * {ve.dm.Node} oldNode * {number} oldIndex * @param {Function} callbacks.move Node moved, arguments: * {ve.dm.Node} newNode * {number} newIndex * {string|null} move * @param {Function} callbacks.changed Node changed, arguments: * {Object} nodeDiff * {ve.dm.Node} oldNode * {ve.dm.Node} newNode * {number} oldIndex * {number} newIndex * {string|null} move */ ve.ui.DiffElement.prototype.iterateDiff = function ( diff, callbacks ) { // Internal list diffs set 'diff' to a number to shortcut computing the list diff // for fully inserted/removed lists. // TODO: Remove this special case and use a regular list diff if ( Array.isArray( diff ) ) { diff.forEach( ( item ) => { let node; switch ( item.diff ) { case 1: node = diff.newList.children[ item.nodeIndex ]; callbacks.insert( node, item.indexOrder ); break; case -1: node = diff.oldList.children[ item.nodeIndex ]; callbacks.remove( node, item.indexOrder ); break; } } ); return; } const len = Math.max( diff.oldNodes.length, diff.newNodes.length ); for ( let i = 0, j = 0; i < len || j < len; i++, j++ ) { const move = diff.moves[ j ] === 0 ? null : diff.moves[ j ]; if ( diff.oldNodes[ i ] === undefined ) { // Everything else in the new doc list is an insert while ( j < diff.newNodes.length ) { callbacks.insert( diff.newNodes[ j ], j ); j++; } } else if ( diff.newNodes[ j ] === undefined ) { // Everything else in the old doc is a remove while ( i < diff.oldNodes.length ) { callbacks.remove( diff.oldNodes[ i ], i ); i++; } } else if ( diff.remove.indexOf( i ) !== -1 ) { // The old node is a remove. Decrement the new node index // to compare the same new node to the next old node callbacks.remove( diff.oldNodes[ i ], i ); j--; } else if ( diff.insert.indexOf( j ) !== -1 ) { // The new node is an insert. Decrement the old node index // to compare the same old node to the next new node callbacks.insert( diff.newNodes[ j ], j ); i--; } else if ( callbacks.preChanged && callbacks.preChanged( diff.oldNodes[ i ], diff.newNodes[ j ], move, i, j ) ) { // preChanged ran } else if ( typeof diff.newToOld[ j ] === 'number' ) { // The old and new node are exactly the same callbacks.move( diff.newNodes[ j ], move, j ); } else { const oldNodeIndex = diff.newToOld[ j ].node; const oldNode = diff.oldNodes[ oldNodeIndex ]; const newNode = diff.newNodes[ diff.oldToNew[ oldNodeIndex ].node ]; const nodeDiff = diff.oldToNew[ oldNodeIndex ].diff; // The new node is modified from the old node callbacks.changed( nodeDiff, oldNode, newNode, move, i, j ); } } }; /** * Get the linear data for the diff of a list-like node that has been changed. * * @param {ve.dm.Node} newListNode Corresponding node from the new document * @param {Object} diff Object describing the diff * @return {Array} Linear data for the diff */ ve.ui.DiffElement.prototype.getChangedListNodeData = function ( newListNode, diff ) { const diffData = []; // These will be adjusted for each item let insertIndex = 1; let depth = -1; const listNodesInfoCache = new Map(); const listDiffItems = []; this.iterateDiff( diff, { insert: ( newNode, index ) => { listDiffItems.push( { node: newNode, metadata: diff.newList.metadata[ index ], action: 'insert' } ); }, remove: ( oldNode, index ) => { listDiffItems.push( { node: oldNode, metadata: diff.oldList.metadata[ index ], action: 'remove' } ); }, move: ( newNode, move, index ) => { listDiffItems.push( { node: newNode, metadata: diff.newList.metadata[ index ], action: 'none', move: move } ); }, changed: ( nodeDiff, oldNode, newNode, move, oldIndex, newIndex ) => { listDiffItems.push( { node: newNode, metadata: diff.newList.metadata[ newIndex ], diff: nodeDiff, move: move } ); } } ); const processedListDiffItems = []; let lastItemSpacer = false; let lastShownDepth = 0; const lastItemAtDepth = {}; listDiffItems.forEach( ( item, i ) => { function isUnchanged( queueItem ) { return !queueItem || ( queueItem.action === 'none' && !queueItem.move ); } if ( isUnchanged( item ) && isUnchanged( listDiffItems[ i - 1 ] ) && isUnchanged( listDiffItems[ i + 1 ] ) ) { if ( !lastItemSpacer ) { processedListDiffItems.push( { node: null, metadata: item.metadata, action: 'none' } ); lastItemSpacer = true; } } else { while ( lastShownDepth < item.metadata.depth - 1 ) { lastShownDepth++; if ( lastItemAtDepth[ lastShownDepth ] ) { processedListDiffItems.push( { node: null, metadata: lastItemAtDepth[ lastShownDepth ].metadata, action: 'none' } ); } } processedListDiffItems.push( item ); lastItemSpacer = false; lastShownDepth = item.metadata.depth; } lastItemAtDepth[ item.metadata.depth ] = item; } ); // Splice in each item with its diff annotations processedListDiffItems.forEach( ( item ) => { let contentData; let isSpacer = false; if ( !item.diff ) { if ( !item.node ) { contentData = [ { type: 'paragraph' }, '…', { type: '/paragraph' } ]; isSpacer = true; this.addAttributesToElement( contentData, 0, { 'data-diff-action': 'none' } ); } else { // Get the linear data for the list item's content contentData = this.getNodeData( item.node, item.action, item.move || null ); } } else { // Item is changed. Get the linear data for the diff contentData = this.getChangedNodeData( item.diff, null, item.node, item.move || null ); } // Calculate the change in depth const newDepth = item.metadata.depth; const depthChange = newDepth - depth; // Get linear data. Also get list node, since may need ancestors const listNode = item.metadata.listNode; let listNodeInfo; if ( !listNodesInfoCache.has( listNode ) ) { // Only re-fetch list node data once per list listNodeInfo = { data: [ this.constructor.static.getDataFromNode( listNode )[ 0 ] ], changeDone: false }; listNodesInfoCache.set( listNode, listNodeInfo ); } else { listNodeInfo = listNodesInfoCache.get( listNode ); } const listItemNode = item.metadata.listItem; // Get linear data of list item let listItemData = this.constructor.static.getDataFromNode( listItemNode ); if ( isSpacer ) { this.addAttributesToElement( listItemData, 0, { 'data-diff-list-spacer': '' } ); } else { // TODO: Make this a node property, instead of a magic attribute if ( listNode.getAttribute( 'style' ) === 'number' ) { // Manually number list items for <ol>'s which contain removals // TODO: Consider if the <ol> contains a `start` attribute (not currently handled by DM) const indexInOwnList = listNode.children.indexOf( listItemNode ); this.addAttributesToElement( listItemData, 0, { value: indexInOwnList + 1 } ); } if ( item.action === 'none' && !item.move ) { this.addAttributesToElement( listItemData, 0, { 'data-diff-list-none': '' } ); } } // e.g. AlienBlockNode, content node is same as 'listItem', so don't duplicate content if ( item.node === listItemNode ) { listItemData = contentData; } else { ve.batchSplice( listItemData, 1, listItemData.length - 2, contentData ); } // Check for attribute changes if ( item.diff && item.diff.attributeChange ) { const attributeChange = { oldAttributes: {}, newAttributes: {} }; [ 'listNodeAttributeChange', 'depthChange', 'listItemAttributeChange' ].forEach( ( listChangeType ) => { if ( item.diff.attributeChange[ listChangeType ] ) { if ( listChangeType === 'listNodeAttributeChange' && depthChange > 0 ) { const change = this.compareNodeAttributes( listNodeInfo.data, 0, item.diff.attributeChange[ listChangeType ] ); if ( change ) { this.descriptionItemsStack.push( change ); listNodeInfo.changeDone = true; } } else if ( listChangeType !== 'listNodeAttributeChange' || !listNodeInfo.changeDone ) { ve.extendObject( attributeChange.oldAttributes, item.diff.attributeChange[ listChangeType ].oldAttributes ); ve.extendObject( attributeChange.newAttributes, item.diff.attributeChange[ listChangeType ].newAttributes ); } } } ); const listItemChange = this.compareNodeAttributes( listItemData, 0, attributeChange ); if ( listItemChange ) { this.descriptionItemsStack.push( listItemChange ); } } if ( item.metadata.isContinued ) { ve.batchSplice( diffData, insertIndex - 1, 0, contentData ); insertIndex += contentData.length; } else { // Record the index to splice in the next list item data into the diffData insertIndex = this.appendListItem( diffData, insertIndex, listNode, listNodeInfo.data, listItemData, depthChange ); } depth = newDepth; } ); return diffData; }; /** * Get the linear data for the diff of a tree-like node that has been changed. * Any node that is not leaf-like or list-like is treated as tree-like. * * @param {ve.dm.Node} oldTreeNode Node from the old document * @param {ve.dm.Node} newTreeNode Corresponding node from the new document * @param {Object} diff Object describing the diff * @return {Array} Linear data for the diff */ ve.ui.DiffElement.prototype.getChangedTreeNodeData = function ( oldTreeNode, newTreeNode, diff ) { const nodeData = this.constructor.static.getDataFromNode( newTreeNode ), nodeRange = newTreeNode.getOuterRange(), treeDiff = diff.treeDiff, diffInfo = diff.diffInfo, oldNodes = diff.oldTreeOrderedNodes, newNodes = diff.newTreeOrderedNodes, correspondingNodes = diff.correspondingNodes, structuralRemoves = [], highestRemovedAncestors = {}; /** * Splice in the removed data for the subtree rooted at this node, from the old * document. * * @param {number} nodeIndex The index of this node in the subtree rooted at * this document child */ const highlightRemovedNode = ( nodeIndex ) => { const findRemovedAncestor = ( n ) => { if ( !n.parent || structuralRemoves.indexOf( n.parent.index ) === -1 ) { return n.index; } else { return findRemovedAncestor( n.parent ); } }; const getRemoveData = ( n, index ) => { const data = this.constructor.static.getDataFromNode( n.node ); this.addAttributesToElement( data, 0, { 'data-diff-action': 'remove' } ); while ( n && n.index !== index ) { n = n.parent; const tempData = this.constructor.static.getDataFromNode( n.node ); data.unshift( tempData[ 0 ] ); data.push( tempData[ tempData.length - 1 ] ); this.addAttributesToElement( data, 0, { 'data-diff-action': 'structural-remove' } ); } return data; }; const orderedNode = oldNodes[ nodeIndex ]; const node = orderedNode.node; if ( node.isDiffedAsTree() && node.hasChildren() ) { // Record that the node has been removed, but don't display it, for now // TODO: describe the change for the attribute diff structuralRemoves.push( nodeIndex ); } else { // Display the removed node, and all its ancestors, up to the first ancestor that // hasn't been removed. const highestRemovedAncestor = oldNodes[ findRemovedAncestor( orderedNode ) ]; const removeData = getRemoveData( orderedNode, highestRemovedAncestor.index ); let insertIndex; // Work out where to insert the removed subtree if ( highestRemovedAncestor.index in highestRemovedAncestors ) { // The highest removed ancestor has already been spliced into nodeData, so remove // it from this subtree and splice the rest of this subtree in removeData.shift(); removeData.pop(); insertIndex = highestRemovedAncestors[ highestRemovedAncestor.index ]; } else if ( !highestRemovedAncestor.parent ) { // If this node is a child of the document node, then it won't have a "previous // node" (see below), in which case, insert it just before its corresponding // node in the new document. insertIndex = newNodes[ correspondingNodes.oldToNew[ highestRemovedAncestor.index ] ] .node.getOuterRange().from - nodeRange.from; } else { // Find the node that corresponds to the "previous node" of this node. The // "previous node" is either: // - the rightmost left sibling that corresponds to a node in the new document // - or if there isn't one, then this node's parent (which must correspond to // a node in the new document, or this node would have been marked already // processed) const siblingNodes = highestRemovedAncestor.parent.children; let newPreviousNodeIndex; for ( let x = 0, xlen = siblingNodes.length; x < xlen; x++ ) { if ( siblingNodes[ x ].index === highestRemovedAncestor.index ) { break; } else { const oldPreviousNodeIndex = siblingNodes[ x ].index; if ( correspondingNodes.oldToNew[ oldPreviousNodeIndex ] !== undefined ) { newPreviousNodeIndex = correspondingNodes.oldToNew[ oldPreviousNodeIndex ]; } } } // If previous node was found among siblings, insert the removed subtree just // after its corresponding node in the new document. Otherwise insert the // removed subtree just inside its parent node's corresponding node. if ( newPreviousNodeIndex !== undefined ) { insertIndex = newNodes[ newPreviousNodeIndex ].node.getOuterRange().to - nodeRange.from; } else { newPreviousNodeIndex = correspondingNodes.oldToNew[ highestRemovedAncestor.parent.index ]; insertIndex = newNodes[ newPreviousNodeIndex ].node.getRange().from - nodeRange.from; } // If more content branch node descendants of the highest removed node have // also been removed, record the index where their subtrees will need to be // spliced in. highestRemovedAncestors[ highestRemovedAncestor.index ] = insertIndex + 1; } ve.batchSplice( nodeData, insertIndex, 0, removeData ); } }; /** * Mark this node as inserted. * * @param {number} nodeIndex The index of this node in the subtree rooted at * this document child */ const highlightInsertedNode = ( nodeIndex ) => { // Find index of first data element for this node const node = newNodes[ nodeIndex ].node; const nodeRangeStart = node.getOuterRange().from - nodeRange.from; // Add insert class this.addAttributesToElement( nodeData, nodeRangeStart, { 'data-diff-action': ( node.isDiffedAsTree() && node.hasChildren() ) ? 'structural-insert' : 'insert' } ); }; /** * Mark this node as changed and, if it is a content branch node, splice in * the diff data. * * @param {number} oldIdx Old node index * @param {number} newIdx New node index * @param {Object} info Diff information relating to this node's change */ const highlightChangedNode = ( oldIdx, newIdx, info ) => { // The new node was changed. // Get data for this node const oldNode = oldNodes[ oldIdx ].node; const newNode = newNodes[ newIdx ].node; const nodeRangeStart = newNode.getOuterRange().from - nodeRange.from; let nodeDiffData = this.getChangedNodeData( info, oldNode, newNode, null, true ); if ( nodeDiffData ) { // Diff was handled e.g. by leaf/list/doc differ ve.batchSplice( nodeData, nodeRangeStart, newNode.getOuterLength(), nodeDiffData ); // TODO: Check if there were actually changes in the sub-diff } else { if ( info.linearDiff ) { // If there is a content change, splice it in nodeDiffData = info.linearDiff; const annotatedData = this.annotateNode( nodeDiffData, newNode ); ve.batchSplice( nodeData, nodeRangeStart + 1, newNode.getLength(), annotatedData ); } if ( info.attributeChange ) { // If there is no content change, just add change class this.addAttributesToElement( nodeData, nodeRangeStart, { 'data-diff-action': 'structural-change' } ); const item = this.compareNodeAttributes( nodeData, nodeRangeStart, info.attributeChange ); if ( item ) { this.descriptionItemsStack.push( item ); } } } }; // Iterate backwards over trees so that changes are made from right to left // of the data, to avoid having to update ranges const len = Math.max( oldNodes.length, newNodes.length ); for ( let i = 0, j = 0; i < len && j < len; i++, j++ ) { const newIndex = newNodes.length - 1 - i; const oldIndex = oldNodes.length - 1 - j; if ( newIndex < 0 ) { // The rest of the nodes have been removed highlightRemovedNode( oldIndex ); } else if ( oldIndex < 0 ) { // The rest of the nodes have been inserted highlightInsertedNode( newIndex ); } else if ( correspondingNodes.newToOld[ newIndex ] === oldIndex ) { // The new node was changed. for ( let k = 0, klen = treeDiff.length; k < klen; k++ ) { if ( treeDiff[ k ][ 0 ] === oldIndex && treeDiff[ k ][ 1 ] === newIndex ) { if ( !diffInfo[ k ] ) { // We are treating these nodes as removed and inserted highlightInsertedNode( newIndex ); highlightRemovedNode( oldIndex ); } else { // There could be any combination of content, attribute and type changes highlightChangedNode( oldIndex, newIndex, diffInfo[ k ] ); } } } } else if ( correspondingNodes.newToOld[ newIndex ] === undefined ) { // The new node was inserted. highlightInsertedNode( newIndex ); j--; } else if ( correspondingNodes.newToOld[ newIndex ] < oldIndex ) { // The old node was removed. highlightRemovedNode( oldIndex ); i--; } } // Push new description items from the queue this.descriptions.addItems( this.descriptionItemsStack ); this.descriptionItemsStack = []; return nodeData; }; /** * Add the relevant attributes to references list node HTML, whether changed or * unchanged (but not inserted or removed - in these cases we just use * getNodeElements). * * @param {HTMLElement} referencesListContainer Div containing the references list * @param {string} action 'change' or 'none' * @param {string|null} move 'up' or 'down' if the node has moved * @param {OO.ui.OptionWidget[]} items Change descriptions for the reference list * @return {HTMLElement[]} Elements to display */ ve.ui.DiffElement.prototype.getRefListNodeElements = function ( referencesListContainer, action, move, items ) { this.markMove( move, referencesListContainer ); this.descriptionItemsStack.push.apply( this.descriptionItemsStack, items ); return [ referencesListContainer ]; }; /** * Get the HTML for the diff of a single internal list item that has been removed * from the old document, inserted into the new document, or that is unchanged. * * @param {ve.dm.InternalItemNode} internalListItemNode Internal list item node * @param {string} action 'remove', 'insert' or 'none' * @param {string|null} move 'up' or 'down' if the node has moved * @param {number} index * @return {HTMLElement[]} Elements (not owned by window.document) */ ve.ui.DiffElement.prototype.getInternalListNodeElements = function ( internalListItemNode, action, move, index ) { const contents = internalListItemNode.children, listItemNode = document.createElement( 'li' ); if ( contents.length ) { contents.forEach( ( node ) => { const elements = this.getNodeElements( node, action, move ); listItemNode.appendChild( listItemNode.ownerDocument.adoptNode( elements[ 0 ] ) ); } ); } else { // TODO: This is MW-Cite-specific behaviour that VE core // should know nothing about. Move to MWDiffElement? $( listItemNode ).append( $( '<span>' ) .addClass( 've-ce-mwReferencesListNode-muted' ) .text( ve.msg( 'cite-ve-referenceslist-missingref-in-list' ) ) ).attr( 'data-diff-action', action ); } listItemNode.setAttribute( 'value', index + 1 ); return [ listItemNode ]; }; /** * Get the HTML for the linear diff of a single internal list item that has changed * from the old document to the new document. * * @param {Object} diff List item diff * @param {ve.dm.InternalItemNode} oldNode * @param {ve.dm.InternalItemNode} newNode * @param {string|null} move 'up' or 'down' if the node has moved * @param {number} newIndex * @return {HTMLElement[]} HTML elements to display the linear diff */ ve.ui.DiffElement.prototype.getInternalListChangedNodeElements = function ( diff, oldNode, newNode, move, newIndex ) { const listItemNode = document.createElement( 'li' ); let data = this.getChangedDocListData( newNode, diff, true ); // Remove internal list wrapper data = data.slice( 1, data.length - 2 ); this.markMove( move, listItemNode ); const newDoc = newNode.getRoot().getDocument(); const documentSlice = newDoc.cloneWithData( data, true, true ); const body = ve.dm.converter.getDomFromModel( documentSlice, ve.dm.Converter.static.PREVIEW_MODE ).body; while ( body.childNodes.length ) { listItemNode.appendChild( listItemNode.ownerDocument.adoptNode( body.childNodes[ 0 ] ) ); } listItemNode.setAttribute( 'value', newIndex + 1 ); return [ listItemNode ]; }; /** * Compare attributes of two nodes * * @param {Array} data Linear data containing new node * @param {number} offset Offset in data * @param {Object} attributeChange Attribute change object containing oldAttributes and newAttributes * @return {OO.ui.OptionWidget|null} Change description item, or null if nothing to describe */ ve.ui.DiffElement.prototype.compareNodeAttributes = function ( data, offset, attributeChange ) { const attributeChanges = this.constructor.static.compareAttributes( attributeChange.oldAttributes, attributeChange.newAttributes ); const changes = ve.dm.modelRegistry.lookup( data[ offset ].type ).static.describeChanges( attributeChanges, attributeChange.newAttributes, data[ offset ] ); // Don't describe the same change twice if ( changes.length && ( !( data[ offset ].internal ) || !( data[ offset ].internal.diff ) || data[ offset ].internal.diff[ 'data-diff-id' ] === undefined ) ) { const item = this.getChangeDescriptionItem( changes ); this.addAttributesToElement( data, offset, { 'data-diff-id': item.getData() } ); return item; } return null; }; /** * Get a change description item from a set of changes * * @param {Array} changes List of changes, each change being either text or a Node array * @param {string[]} classes Additional classes * @return {OO.ui.OptionWidget} Change description item */ ve.ui.DiffElement.prototype.getChangeDescriptionItem = function ( changes, classes ) { const elementId = this.elementId; let $label = $( [] ); for ( let i = 0, l = changes.length; i < l; i++ ) { const $change = $( '<div>' ); if ( typeof changes[ i ] === 'string' ) { $change.text( changes[ i ] ); } else { // changes[ i ] is definitely not an HTML string in this branch // eslint-disable-next-line no-jquery/no-append-html $change.append( changes[ i ] ); } $label = $label.add( $change ); } // eslint-disable-next-line mediawiki/class-doc const item = new OO.ui.OptionWidget( { label: $label, data: elementId, classes: [ 've-ui-diffElement-attributeChange', ...classes || [] ] } ); this.elementId++; return item; }; /** * Mark an element with attributes to be added later by the converter. * * @param {Array} data Data containing element to be marked * @param {number} offset Offset of element to be marked * @param {Object} attributes Attributes to set */ ve.ui.DiffElement.prototype.addAttributesToElement = function ( data, offset, attributes ) { const newElement = ve.copy( data[ offset ] ); // NB we modify the linear data here, but then this is a cloned document. for ( const key in attributes ) { if ( attributes[ key ] !== undefined ) { ve.setProp( newElement, 'internal', 'diff', key, attributes[ key ] ); } } // Don't let any nodes get unwrapped ve.deleteProp( newElement, 'internal', 'generated' ); data.splice( offset, 1, newElement ); }; /** * Mark an HTML element or data element as moved * * @param {string|null} move 'up' or 'down' if the node has moved * @param {HTMLElement|Array} elementOrData Linear data or HTMLElement * @param {number} [offset=0] Linear mode offset */ ve.ui.DiffElement.prototype.markMove = function ( move, elementOrData, offset ) { if ( !move ) { return; } // The following messages are used here: // * visualeditor-diff-moved-up // * visualeditor-diff-moved-down // The following classes are used here: // * ve-ui-diffElement-moved-up // * ve-ui-diffElement-moved-down const item = this.getChangeDescriptionItem( [ ve.msg( 'visualeditor-diff-moved-' + move ) ], [ 've-ui-diffElement-moved-' + move ] ); if ( Array.isArray( elementOrData ) ) { this.addAttributesToElement( elementOrData, offset || 0, { 'data-diff-move': move, 'data-diff-id': item.getData() } ); } else { elementOrData.setAttribute( 'data-diff-move', move ); elementOrData.setAttribute( 'data-diff-id', item.getData() ); } this.descriptionItemsStack.push( item ); }; /** * Annotate some data to highlight diff * * @param {Array} linearDiff Linear diff, mapping arrays of linear data to diff * actions (remove, insert or retain) * @param {ve.dm.Node} newNode Node from the new document * @return {Array} Data with annotations added */ ve.ui.DiffElement.prototype.annotateNode = function ( linearDiff, newNode ) { const DIFF_DELETE = ve.DiffMatchPatch.static.DIFF_DELETE, DIFF_INSERT = ve.DiffMatchPatch.static.DIFF_INSERT, DIFF_CHANGE_DELETE = ve.DiffMatchPatch.static.DIFF_CHANGE_DELETE, DIFF_CHANGE_INSERT = ve.DiffMatchPatch.static.DIFF_CHANGE_INSERT, items = [], newDoc = newNode.getRoot().getDocument(); let start = 0; // The starting index for a range for building an annotation // Make a new document from the diff const diffDocData = linearDiff[ 0 ][ 1 ].slice(); const ilen = linearDiff.length; for ( let i = 1; i < ilen; i++ ) { ve.batchPush( diffDocData, linearDiff[ i ][ 1 ] ); } const diffDoc = newDoc.cloneWithData( diffDocData ); // Add spans with the appropriate attributes for removes and inserts // TODO: do insert and remove outside of loop for ( let i = 0; i < ilen; i++ ) { const end = start + linearDiff[ i ][ 1 ].length; if ( start !== end ) { const range = new ve.Range( start, end ); const type = linearDiff[ i ][ 0 ]; if ( type !== 0 ) { let typeAsString, domElementType, annType; switch ( type ) { case DIFF_DELETE: typeAsString = 'remove'; domElementType = 'del'; annType = 'textStyle/delete'; break; case DIFF_INSERT: typeAsString = 'insert'; domElementType = 'ins'; annType = 'textStyle/insert'; break; case DIFF_CHANGE_DELETE: typeAsString = 'change-remove'; domElementType = 'span'; annType = 'textStyle/span'; break; case DIFF_CHANGE_INSERT: typeAsString = 'change-insert'; domElementType = 'span'; annType = 'textStyle/span'; break; } const domElement = document.createElement( domElementType ); domElement.setAttribute( 'data-diff-action', typeAsString ); const domElements = [ domElement ]; const changes = []; if ( linearDiff[ i ].annotationChanges ) { linearDiff[ i ].annotationChanges.forEach( ( annotationChange ) => { let attributeChanges; if ( annotationChange.oldAnnotation && annotationChange.newAnnotation ) { attributeChanges = this.constructor.static.compareAttributes( annotationChange.oldAnnotation.getAttributes(), annotationChange.newAnnotation.getAttributes() ); ve.batchPush( changes, ve.dm.modelRegistry.lookup( annotationChange.newAnnotation.getType() ).static.describeChanges( attributeChanges, annotationChange.newAnnotation.getAttributes(), annotationChange.newAnnotation.getElement() ) ); } else if ( annotationChange.newAnnotation ) { ve.batchPush( changes, annotationChange.newAnnotation.describeAdded() ); } else if ( annotationChange.oldAnnotation ) { ve.batchPush( changes, annotationChange.oldAnnotation.describeRemoved() ); } } ); } if ( linearDiff[ i ].attributeChanges ) { const element = linearDiff[ i ][ 1 ][ 0 ]; linearDiff[ i ].attributeChanges.forEach( ( attributeChange ) => { const attributeChanges = this.constructor.static.compareAttributes( attributeChange.oldAttributes, attributeChange.newAttributes ); ve.batchPush( changes, ve.dm.modelRegistry.lookup( element.type ).static.describeChanges( attributeChanges, element.attributes, element ) ); } ); } if ( changes.length ) { const item = this.getChangeDescriptionItem( changes ); domElement.setAttribute( 'data-diff-id', item.getData() ); items.push( item ); } const originalDomElementsHash = diffDoc.getStore().hash( domElements, domElements.map( ve.getNodeHtml ).join( '' ) ); const annHash = diffDoc.getStore().hash( ve.dm.annotationFactory.create( annType, { type: annType, originalDomElementsHash: originalDomElementsHash } ) ); // Insert annotation above annotations that span the entire range // and at least one character more const annHashLists = []; for ( let j = Math.max( 0, range.start - 1 ); j < Math.min( range.end + 1, diffDoc.data.getLength() ); j++ ) { annHashLists[ j ] = diffDoc.data.getAnnotationHashesFromOffset( j ); } const height = Math.min( ve.getCommonStartSequenceLength( annHashLists.slice( Math.max( 0, range.start - 1 ), range.end ) ), ve.getCommonStartSequenceLength( annHashLists.slice( range.start, Math.min( range.end + 1, diffDoc.data.getLength() ) ) ) ); for ( let j = range.start; j < range.end; j++ ) { annHashLists[ j ].splice( height, 0, annHash ); diffDoc.data.setAnnotationHashesAtOffset( j, annHashLists[ j ] ); } } } start = end; } this.descriptionItemsStack.push.apply( this.descriptionItemsStack, items ); // Merge the stores and get the data newDoc.getStore().merge( diffDoc.getStore() ); const annotatedLinearDiff = diffDoc.getData( { start: 0, end: diffDoc.getLength() } ); return annotatedLinearDiff; }; ve.ui.metaListDiffRegistry = new OO.Registry();
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка