view/resources/wikibase/templates.js
/**
* @license GPL-2.0-or-later
* @author H. Snater < mediawiki@snater.com >
*/
( function () {
'use strict';
/**
* Template cache that stores the parameter types templates have been generated with. These
* templates do not need to be validated anymore allowing to skip the validation process.
*
* @type {Object}
*/
var cache = {};
/**
* Returns the type of the specified parameter.
*
* @param {*} param
* @return {string}
*/
function getParameterType( param ) {
return ( param instanceof $ ) ? 'jQuery' : typeof param;
}
/**
* Checks whether a specific template has been initialized with the types of the specified
* parameters before.
*
* @param {string} key Template id.
* @param {*[]} params
* @return {boolean}
*/
function areCachedParameterTypes( key, params ) {
if ( !cache[ key ] ) {
return false;
}
for ( var i = 0; i < cache[ key ].length; i++ ) {
if ( params.length !== cache[ key ][ i ].length ) {
return false;
}
for ( var j = 0; j < params.length; j++ ) {
if ( getParameterType( params[ j ] ) !== cache[ key ][ i ][ j ] ) {
return false;
}
}
}
return true;
}
/**
* Strips HTML tags that may be generated automatically like <tbody> as well as all node
* attributes.
*
* @param {string} string
* @return {string}
*/
function stripAutoGeneratedHtml( string ) {
string = string.replace( /<\/?t(?:head|body|foot)\b[^>]*>/gi, '' );
// strip white space between tags as well since it might cause interference
string = string.replace( />\s+</g, '><' );
// Strip all attributes since they are not necessary for validating the HTML and would cause
// interference in Firefox which re-converts < and > back to < and > when parsing by
// setting through $.html().
// Additionally, rip off any XML notation since jQuery will parse to HTML.
// eslint-disable-next-line security/detect-unsafe-regex -- TODO review
return string.replace( /(<\w+)\b(?:[^"'>]+|"[^"]*"|'[^']*')*>/g, '$1>' );
}
/**
* Checks whether the HTML to be created out of a jQuery wrapped element is actually valid.
*
* @param {string} template HTML
* @param {jQuery} $wrappedTemplate
* @return {boolean}
*/
function isValidHtml( template, $wrappedTemplate ) {
// HTML node automatically creates a body tag for certain elements that fit right into the
// body - not for tags like <tr>:
var parsedTemplate = ( $wrappedTemplate.children( 'body' ).length )
? $wrappedTemplate.children( 'body' ).html()
: $wrappedTemplate.html();
var strippedTemplate = stripAutoGeneratedHtml( template ),
strippedParsedTemplate = stripAutoGeneratedHtml( parsedTemplate );
// Unescape remaining quotes in our template since all attributes are gone
// and jQuery does not escape quotes in text nodes
var strippedTemplateWithUnescapedQuotes = strippedTemplate.replace(
/"|'/g,
function ( escapedQuote ) {
return escapedQuote === '"' ? '"' : "'";
}
);
// Nodes or text got lost while being parsed which indicates that the generated HTML would
// be invalid:
return strippedTemplateWithUnescapedQuotes === strippedParsedTemplate;
}
/**
* Adds a template to the cache.
*
* @param {string} key Template id.
* @param {*[]} params Original template parameters.
*/
function addToCache( key, params ) {
var paramTypes = [];
if ( !cache[ key ] ) {
cache[ key ] = [];
}
for ( var i = 0; i < params.length; i++ ) {
var parameterType = getParameterType( params[ i ] );
if ( parameterType === 'object' ) {
// Cannot handle some generic object.
return;
} else {
paramTypes.push( parameterType );
}
}
cache[ key ].push( paramTypes );
}
/**
* Returns a template filled with the specified parameters, similar to wfTemplate().
*
* @param {string} key Key of the template to get.
* @param {string|string[]|jQuery} [parameter1] First argument in a list of variadic arguments,
* each a parameter for $N replacement in templates. Instead of making use of variadic
* arguments, an array may be passed as first parameter.
* @return {jQuery}
*
* @throws {Error} if the generated template's HTML is invalid.
* @internal Wikibase JavaScript code is not considered a stable interface.
*/
mw.wbTemplate = function ( key, parameter1 /* [, parameter2[, ...]] */ ) {
var i,
params = [],
template,
$wrappedTemplate,
tempParams = [],
delayedParams = [];
if ( parameter1 !== undefined ) {
if ( Array.isArray( parameter1 ) ) {
params = parameter1;
} else { // support variadic arguments
params = Array.prototype.slice.call( arguments );
params.shift();
}
}
// Pre-parse the template inserting strings and placeholder nodes for jQuery objects jQuery
// objects will be appended after the template has been parsed to not lose any references:
for ( i = 0; i < params.length; i++ ) {
if ( typeof params[ i ] === 'string' || params[ i ] instanceof String ) {
// insert strings into the template directly
tempParams.push( mw.html.escape( params[ i ] ) );
} else if ( params[ i ] instanceof $ ) {
// construct temporary placeholder nodes
// (using an actual invalid class name to not interfere with any other node)
var nodeName = params[ i ][ 0 ].nodeName.toLowerCase();
tempParams.push( '<' + nodeName + ' class="--mwTemplate"></' + nodeName + '>' );
delayedParams.push( params[ i ] );
} else {
throw new Error( 'mw.wbTemplate: Wrong parameter type. Pass either String or jQuery.' );
}
}
template = mw.wbTemplates.store.get( key );
var html;
if ( !template ) {
html = '⧼' + mw.html.escape( key ) + '⧽';
} else {
html = mw.format.apply( null, [ template ].concat( tempParams ) );
}
// Wrap template inside a html container to be able to easily access all temporary nodes and
// insert any jQuery objects:
$wrappedTemplate = $( '<html>' ).html( html );
if ( !areCachedParameterTypes( key, params ) ) {
if ( !isValidHtml( html, $wrappedTemplate ) ) {
throw new Error( 'mw.wbTemplate: Tried to generate invalid HTML for template "'
+ key + '"' );
}
addToCache( key, params );
}
// Replace temporary nodes with actual jQuery nodes:
$wrappedTemplate.find( '.--mwTemplate' ).each( function ( j ) {
$( this ).replaceWith( delayedParams[ j ] );
} );
return ( $wrappedTemplate.children( 'body' ).length )
? $wrappedTemplate.children( 'body' ).contents()
: $wrappedTemplate.contents();
};
/**
* Fetches a template and fills it with specified parameters. The template has to have a single
* root DOM element. All of its child nodes will then be appended to the jQuery object's DOM
* nodes.
*
* @param {string} template
* @param {string|string[]|jQuery} parameter1 First argument in a list of variadic arguments,
* each a parameter for $N replacement in templates. Instead of making use of variadic
* arguments, an array may be passed as first parameter.
* @return {jQuery}
*/
$.fn.applyTemplate = function ( template, parameter1 /* [, parameter2[, ...]] */ ) {
var $template = mw.wbTemplate.apply( null, arguments );
if ( $template.length !== 1 ) {
throw new Error( 'Can not apply a template with more or less than one root node.' );
}
var attribs = $template[ 0 ].attributes;
for ( var i = 0; i < attribs.length; i++ ) {
if ( attribs[ i ].name === 'class' ) {
this.addClass( attribs[ i ].value );
} else {
this.attr( attribs[ i ].name, attribs[ i ].value );
}
}
this.empty().append( $template.contents() );
return this;
};
}() );