/*! * VisualEditor DataModel MWExtensionNode class. * * @copyright See AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * DataModel MediaWiki extension node. * * @class * @abstract * @extends ve.dm.LeafNode * @mixes ve.dm.FocusableNode * @mixes ve.dm.GeneratedContentNode * * @constructor */ ve.dm.MWExtensionNode = function VeDmMWExtensionNode() { // Parent constructor ve.dm.MWExtensionNode.super.apply( this, arguments ); // Mixin constructors ve.dm.GeneratedContentNode.call( this ); ve.dm.FocusableNode.call( this ); }; /* Inheritance */ OO.inheritClass( ve.dm.MWExtensionNode, ve.dm.LeafNode ); OO.mixinClass( ve.dm.MWExtensionNode, ve.dm.FocusableNode ); OO.mixinClass( ve.dm.MWExtensionNode, ve.dm.GeneratedContentNode ); /* Static members */ ve.dm.MWExtensionNode.static.enableAboutGrouping = true; ve.dm.MWExtensionNode.static.matchTagNames = null; ve.dm.MWExtensionNode.static.childNodeTypes = []; /** * HTML tag name. * * @static * @property {string} * @inheritable */ ve.dm.MWExtensionNode.static.tagName = null; /** * Name of the MediaWiki parser extension tag. (Not related to the name of the MediaWiki extension.) * * @static * @property {string} * @inheritable */ ve.dm.MWExtensionNode.static.extensionName = null; ve.dm.MWExtensionNode.static.getMatchRdfaTypes = function () { return [ 'mw:Extension/' + this.extensionName ]; }; /** * @inheritdoc * @param {Node[]} domElements * @param {ve.dm.Converter} converter * @param {string} [type] Type to give dataElement, defaults to static.name */ ve.dm.MWExtensionNode.static.toDataElement = function ( domElements, converter, type ) { const mwDataJSON = domElements[ 0 ].getAttribute( 'data-mw' ), mwData = mwDataJSON ? JSON.parse( mwDataJSON ) : {}; const dataElement = { type: type || this.name, attributes: { mw: mwData, originalMw: mwDataJSON } }; this.storeGeneratedContents( dataElement, domElements, converter.getStore() ); // Sub-classes should not modify dataElement beyond this point as it will invalidate the cache return dataElement; }; /** * @inheritdoc ve.dm.Node */ ve.dm.MWExtensionNode.static.cloneElement = function () { // Parent method const clone = ve.dm.MWExtensionNode.super.static.cloneElement.apply( this, arguments ); delete clone.attributes.originalMw; return clone; }; ve.dm.MWExtensionNode.static.toDomElements = function ( dataElement, doc, converter ) { const originalMw = dataElement.attributes.originalMw; let els; // If the transclusion is unchanged just send back the // original DOM elements so selser can skip over it if ( dataElement.originalDomElementsHash && originalMw && ve.compare( dataElement.attributes.mw, JSON.parse( originalMw ) ) ) { // originalDomElements is also used for CE rendering so return a copy els = ve.copyDomElements( converter.getStore().value( dataElement.originalDomElementsHash ), doc ); } else { const store = converter.getStore(); let value; if ( converter.doesModeNeedRendering() && // Use getHashObjectForRendering to get the rendering from the store ( value = store.value( store.hashOfValue( null, OO.getHash( [ this.getHashObjectForRendering( dataElement ), undefined ] ) ) ) ) ) { // For the clipboard use the current DOM contents so the user has something // meaningful to paste into external applications els = ve.copyDomElements( value, doc ); } else { const el = doc.createElement( this.tagName ); el.setAttribute( 'typeof', 'mw:Extension/' + this.getExtensionName( dataElement ) ); el.setAttribute( 'data-mw', JSON.stringify( dataElement.attributes.mw ) ); els = [ el ]; } } return els; }; ve.dm.MWExtensionNode.static.getHashObject = function ( dataElement ) { return { type: dataElement.type, mw: ve.copy( dataElement.attributes.mw ) }; }; /** * Get name of the MediaWiki parser extension tag. * * Static version for toDomElements * * @static * @param {Object} dataElement Data element * @return {string} Extension name */ ve.dm.MWExtensionNode.static.getExtensionName = function () { return this.extensionName; }; ve.dm.MWExtensionNode.static.describeChanges = function ( attributeChanges, attributes, element ) { const descriptions = [], fromBody = attributeChanges.mw.from.body, toBody = attributeChanges.mw.to.body; if ( attributeChanges.mw ) { // HACK: Try to generate an ' has changed' message using the associated tool's title const tools = ve.ui.toolFactory.getRelatedItems( [ ve.dm.nodeFactory.createFromElement( element ) ] ); if ( tools.length ) { descriptions.push( ve.msg( 'visualeditor-changedesc-unknown', OO.ui.resolveMsg( ve.ui.toolFactory.lookup( tools[ 0 ].name ).static.title ) ) ); } // Compare body - default behaviour in #describeChange does nothing if ( !ve.compare( fromBody, toBody ) ) { const change = this.describeChange( 'body', { from: fromBody && fromBody.extsrc, to: toBody && toBody.extsrc } ); if ( change ) { descriptions.push( change ); } } // Append attribute changes // Parent method Array.prototype.push.apply( descriptions, ve.dm.MWExtensionNode.super.static.describeChanges.call( this, ve.ui.DiffElement.static.compareAttributes( attributeChanges.mw.from.attrs || {}, attributeChanges.mw.to.attrs || {} ), attributes ) ); return descriptions; } // 'mw' should be the only attribute that changes... return []; }; ve.dm.MWExtensionNode.static.describeChange = function ( key, change ) { if ( key === 'body' ) { if ( change.from && change.to ) { const store = new ve.dm.HashValueStore(); const linearDiffer = new ve.DiffMatchPatch( store, store ); const trimNewlines = /^\n+|\n+$/g; const linearDiff = linearDiffer.getCleanDiff( change.from.replace( trimNewlines, '' ).split( '' ), change.to.replace( trimNewlines, '' ).split( '' ), { keepOldText: false } ); const div = document.createElement( 'div' ); linearDiff.forEach( ( diffSection, i ) => { const [ type, data ] = diffSection; const text = data.join( '' ); let el, nodeText; switch ( type ) { case ve.DiffMatchPatch.static.DIFF_DELETE: el = document.createElement( 'del' ); nodeText = text; break; case ve.DiffMatchPatch.static.DIFF_INSERT: el = document.createElement( 'ins' ); nodeText = text; break; case ve.DiffMatchPatch.static.DIFF_EQUAL: { el = document.createElement( 'span' ); const lines = text.split( '\n' ); const filteredLines = []; if ( lines.length === 1 ) { nodeText = text; } else { if ( i !== 0 ) { filteredLines.push( lines[ 0 ] ); } if ( lines.length > 2 ) { filteredLines.push( '…' ); } if ( i !== linearDiff.length - 1 ) { filteredLines.push( lines[ lines.length - 1 ] ); } nodeText = filteredLines.join( '\n' ); } break; } } el.appendChild( document.createTextNode( nodeText ) ); div.appendChild( el ); } ); return [ div ]; } return null; } // Parent method return ve.dm.MWExtensionNode.super.static.describeChange.apply( this, arguments ); }; /* Methods */ /** * Get name of the MediaWiki parser extension tag. * * @return {string} Extension name */ ve.dm.MWExtensionNode.prototype.getExtensionName = function () { return this.constructor.static.getExtensionName( this.element ); };