mediawiki-extensions-Visual.../modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js
Roan Kattouw bb45d984ca dm.MWInternalLinkAnnotation: Fix href normalization for special characters
<a href="Foo%3F"> would dirty-diff to <a href="Foo?"> and also render
as such, pointing to the wrong page.

We also called decodeURIComponent() on the href twice, which can't
have been good.

Move URI decoding and underscore normalization into
getTargetDataFromHref(), and add rawTitle for callers that need it.
Put rawTitle in the origTitle attribute, so that equivalence
comparisons (decode(origTitle) === title) work as intended.

Bug: T145978
Change-Id: I29331a4ab0f8f7ef059c109f6813fa670a2c7390
2016-09-22 09:10:41 -07:00

277 lines
8.1 KiB
JavaScript

/*!
* VisualEditor ContentEditable MWTransclusionNode class.
*
* @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* ContentEditable MediaWiki transclusion node.
*
* @class
* @abstract
* @extends ve.ce.LeafNode
* @mixins ve.ce.GeneratedContentNode
* @mixins ve.ce.FocusableNode
* @mixins ve.ce.TableCellableNode
*
* @constructor
* @param {ve.dm.MWTransclusionNode} model Model to observe
* @param {Object} [config] Configuration options
*/
ve.ce.MWTransclusionNode = function VeCeMWTransclusionNode( model, config ) {
// Parent constructor
ve.ce.MWTransclusionNode.super.call( this, model, config );
// Mixin constructors
ve.ce.GeneratedContentNode.call( this );
ve.ce.FocusableNode.call( this );
ve.ce.TableCellableNode.call( this );
};
/* Inheritance */
OO.inheritClass( ve.ce.MWTransclusionNode, ve.ce.LeafNode );
OO.mixinClass( ve.ce.MWTransclusionNode, ve.ce.GeneratedContentNode );
OO.mixinClass( ve.ce.MWTransclusionNode, ve.ce.FocusableNode );
OO.mixinClass( ve.ce.MWTransclusionNode, ve.ce.TableCellableNode );
/* Static Properties */
ve.ce.MWTransclusionNode.static.name = 'mwTransclusion';
ve.ce.MWTransclusionNode.static.renderHtmlAttributes = false;
ve.ce.MWTransclusionNode.static.primaryCommandName = 'transclusion';
ve.ce.MWTransclusionNode.static.iconWhenInvisible = 'template';
/* Static Methods */
/**
* Get a list of descriptions of template parts in a transclusion node
*
* @static
* @param {ve.dm.Node} model Node model
* @return {string[]} List of template part descriptions
*/
ve.ce.MWTransclusionNode.static.getTemplatePartDescriptions = function ( model ) {
var i, len, part,
parts = model.getPartsList(),
words = [];
for ( i = 0, len = parts.length; i < len; i++ ) {
part = parts[ i ];
if ( part.template ) {
words.push( part.template );
}
}
return words;
};
/**
* @inheritdoc
*/
ve.ce.MWTransclusionNode.static.getDescription = function ( model ) {
return this.getTemplatePartDescriptions( model )
.map( function ( template ) {
var title = mw.Title.newFromText( template, mw.config.get( 'wgNamespaceIds' ).template );
if ( title ) {
return title.getRelativeText( mw.config.get( 'wgNamespaceIds' ).template );
} else {
return template;
}
} )
.join( ve.msg( 'comma-separator' ) );
};
/**
* Filter rendering to remove auto-generated content and wrappers
*
* @static
* @param {HTMLElement[]} contentNodes Rendered nodes
* @return {HTMLElement[]} Filtered rendered nodes
*/
ve.ce.MWTransclusionNode.static.filterRendering = function ( contentNodes ) {
// Filter out auto-generated items, e.g. reference lists
contentNodes = contentNodes.filter( function ( node ) {
var dataMw = node.nodeType === Node.ELEMENT_NODE &&
node.hasAttribute( 'data-mw' ) &&
JSON.parse( node.getAttribute( 'data-mw' ) );
if ( dataMw && dataMw.autoGenerated ) {
return false;
}
return true;
} );
// HACK: if $content consists of a single paragraph, unwrap it.
// We have to do this because the parser wraps everything in <p>s, and inline templates
// will render strangely when wrapped in <p>s.
if ( contentNodes.length === 1 && contentNodes[ 0 ].nodeName.toLowerCase() === 'p' ) {
contentNodes = Array.prototype.slice.apply( contentNodes[ 0 ].childNodes );
}
return contentNodes;
};
/* Methods */
/**
* @inheritdoc
*/
ve.ce.MWTransclusionNode.prototype.createInvisibleIcon = function () {
var icon = new OO.ui.ButtonWidget( {
classes: [ 've-ce-focusableNode-invisibleIcon' ],
framed: false,
icon: this.constructor.static.iconWhenInvisible,
label: this.constructor.static.getDescription( this.getModel() )
} );
return icon.$element;
};
/**
* @inheritdoc
*/
ve.ce.MWTransclusionNode.prototype.generateContents = function ( config ) {
var xhr, deferred = $.Deferred();
xhr = new mw.Api().post( {
action: 'visualeditor',
paction: 'parsefragment',
page: mw.config.get( 'wgRelevantPageName' ),
wikitext: ( config && config.wikitext ) || this.model.getWikitext(),
pst: 1
} )
.done( this.onParseSuccess.bind( this, deferred ) )
.fail( this.onParseError.bind( this, deferred ) );
return deferred.promise( { abort: xhr.abort } );
};
/**
* Handle a successful response from the parser for the wikitext fragment.
*
* @param {jQuery.Deferred} deferred The Deferred object created by #generateContents
* @param {Object} response Response data
*/
ve.ce.MWTransclusionNode.prototype.onParseSuccess = function ( deferred, response ) {
var contentNodes;
if ( ve.getProp( response, 'visualeditor', 'result' ) !== 'success' ) {
return this.onParseError.call( this, deferred );
}
// Work around https://github.com/jquery/jquery/issues/1997
contentNodes = $.parseHTML( response.visualeditor.content, this.getModelHtmlDocument() ) || [];
deferred.resolve( this.constructor.static.filterRendering( contentNodes ) );
};
/**
* Extend the ve.ce.GeneratedContentNode render method to check for hidden templates.
*
* Check if the final result of the imported template is empty.
*
* @see ve.ce.GeneratedContentNode#render
*/
ve.ce.MWTransclusionNode.prototype.render = function ( generatedContents ) {
// Call parent mixin
ve.ce.GeneratedContentNode.prototype.render.call( this, generatedContents );
};
/**
* @inheritdoc
*/
ve.ce.MWTransclusionNode.prototype.onSetup = function () {
// Parent method
ve.ce.MWTransclusionNode.super.prototype.onSetup.apply( this, arguments );
// Render replaces this.$element with a new node so re-add classes
this.$element.addClass( 've-ce-mwTransclusionNode' );
};
/**
* @inheritdoc
*/
ve.ce.MWTransclusionNode.prototype.getRenderedDomElements = function ( domElements ) {
var $elements = $( ve.ce.GeneratedContentNode.prototype.getRenderedDomElements.call( this, domElements ) ),
transclusionNode = this;
if ( this.getModelHtmlDocument() ) {
$elements
.find( 'a[href][rel="mw:WikiLink"]' ).addBack( 'a[href][rel="mw:WikiLink"]' )
.each( function () {
var targetData = ve.dm.MWInternalLinkAnnotation.static.getTargetDataFromHref(
this.href, transclusionNode.getModelHtmlDocument()
),
normalisedHref = ve.decodeURIComponentIntoArticleTitle( targetData.rawTitle, false );
if ( mw.Title.newFromText( normalisedHref ) ) {
normalisedHref = mw.Title.newFromText( normalisedHref ).getPrefixedText();
}
ve.init.platform.linkCache.styleElement( normalisedHref, $( this ) );
} );
}
return $elements.toArray();
};
/**
* Handle an unsuccessful response from the parser for the wikitext fragment.
*
* @param {jQuery.Deferred} deferred The promise object created by #generateContents
* @param {Object} response Response data
*/
ve.ce.MWTransclusionNode.prototype.onParseError = function ( deferred ) {
deferred.reject();
};
/* Concrete subclasses */
/**
* ContentEditable MediaWiki transclusion block node.
*
* @class
* @extends ve.ce.MWTransclusionNode
* @constructor
* @param {ve.dm.MWTransclusionBlockNode} model Model to observe
*/
ve.ce.MWTransclusionBlockNode = function VeCeMWTransclusionBlockNode( model ) {
// Parent constructor
ve.ce.MWTransclusionBlockNode.super.call( this, model );
};
/* Inheritance */
OO.inheritClass( ve.ce.MWTransclusionBlockNode, ve.ce.MWTransclusionNode );
/* Static Properties */
ve.ce.MWTransclusionBlockNode.static.name = 'mwTransclusionBlock';
ve.ce.MWTransclusionBlockNode.static.tagName = 'div';
/**
* ContentEditable MediaWiki transclusion inline node.
*
* @class
* @extends ve.ce.MWTransclusionNode
* @constructor
* @param {ve.dm.MWTransclusionInlineNode} model Model to observe
*/
ve.ce.MWTransclusionInlineNode = function VeCeMWTransclusionInlineNode( model ) {
// Parent constructor
ve.ce.MWTransclusionInlineNode.super.call( this, model );
};
/* Inheritance */
OO.inheritClass( ve.ce.MWTransclusionInlineNode, ve.ce.MWTransclusionNode );
/* Static Properties */
ve.ce.MWTransclusionInlineNode.static.name = 'mwTransclusionInline';
ve.ce.MWTransclusionInlineNode.static.tagName = 'span';
/* Registration */
ve.ce.nodeFactory.register( ve.ce.MWTransclusionNode );
ve.ce.nodeFactory.register( ve.ce.MWTransclusionBlockNode );
ve.ce.nodeFactory.register( ve.ce.MWTransclusionInlineNode );