Source: ext.templateDataGenerator.data/SourceHandler.js

var Model = require( './Model.js' );

/**
 * TemplateData Source Handler
 *
 * Loads and validates the templatedata and template parameters
 * whether in the page itself or from its parent.
 *
 * @class
 * @extends OO.EventEmitter
 *
 * @constructor
 * @param {Object} [config]
 * @cfg {string} [fullPageName] The full name of the current page
 * @cfg {string} [parentPage] The name of the parent page
 * @cfg {string} [isPageSubLevel] The page is sub-level of another template
 */
function SourceHandler( config ) {
	config = config || {};

	// Mixin constructors
	OO.EventEmitter.call( this );

	this.apiCache = {};
	this.templateSourceCodePromise = null;
	this.templateSourceCodeParams = [];

	// Config
	this.setParentPage( config.parentPage );
	this.setPageSubLevel( config.isPageSubLevel );
	this.setFullPageName( config.fullPageName );
}

/* Inheritance */

OO.mixinClass( SourceHandler, OO.EventEmitter );

/**
 * Get information from the MediaWiki API
 *
 * @param {string} page Page name
 * @param {boolean} [getTemplateData] Fetch the templatedata in the page.
 * @return {jQuery.Promise} API promise
 */
SourceHandler.prototype.getApi = function ( page, getTemplateData ) {
	var type = getTemplateData ? 'templatedata' : 'query',
		api = new mw.Api(),
		baseConfig = {
			action: type,
			titles: page,
			redirects: getTemplateData ? 1 : 0
		};

	var config;
	if ( type === 'query' ) {
		config = $.extend( baseConfig, {
			prop: 'revisions',
			rvprop: 'content',
			indexpageids: '1'
		} );
	}

	// Cache
	if ( !this.apiCache[ page ] || !this.apiCache[ page ][ type ] ) {
		this.apiCache[ page ] = this.apiCache[ page ] || {};
		this.apiCache[ page ][ type ] = api.get( config );
	}
	return this.apiCache[ page ][ type ];
};

/**
 * Go over the current wikitext and build a new model.
 *
 * @param {string} [wikitext] Source of the template.
 * @return {jQuery.Promise} Promise resolving into a new Model
 *  or is rejected if the model was impossible to create.
 */
SourceHandler.prototype.buildModel = function ( wikitext ) {
	var tdObject = null,
		templateDataString = this.findModelInString( wikitext );

	if ( templateDataString !== null ) {
		try {
			tdObject = JSON.parse( templateDataString );
		} catch ( err ) {
			// The json object is invalid. There's no need to continue.
			return $.Deferred().reject();
		}
	}

	// Get parameters from source code
	// Mostly used for the import option
	return this.getParametersFromTemplateSource( wikitext )
		// This is always successful by definition
		.then( function ( templateSourceCodeParams ) {
			return Model.static.newFromObject(
				tdObject,
				templateSourceCodeParams
			);
		} );
};

/**
 * Retrieve parameters from the template code from source in this order:
 *
 * 1. Check if there's a template in the given 'wikitext' parameter. If not,
 * 2. Check if there's a template in the current page. If not,
 * 3. Check if the page is a subpage and go up a level to check for template code. If none found,
 * 4. Repeat until we are in the root of the template
 * 5. Save the name of the page where the template is taken from
 *
 * Cache the templateCodePromise so we don't have to do this all over again on each
 * template code request.
 *
 * @param {string} [wikitext] Optional. Source of the template.
 * @return {jQuery.Promise} Promise resolving into template parameter array
 */
SourceHandler.prototype.getParametersFromTemplateSource = function ( wikitext ) {
	var params = [],
		sourceHandler = this;

	if ( !this.templateSourceCodePromise ) {
		// Check given page text first
		if ( wikitext ) {
			params = this.extractParametersFromTemplateCode( wikitext );
		}

		if ( params.length > 0 ) {
			// There are parameters found; Resolve.
			this.templateSourceCodePromise = $.Deferred().resolve( params );
		} else if ( this.isPageSubLevel() && this.getParentPage() ) {
			// Get the content of the parent
			this.templateSourceCodePromise = this.getApi( this.getParentPage() ).then(
				function ( resp ) {
					var pageContent = '';

					// Verify that we have a sane response from the API.
					// This is particularly important for unit tests, since the
					// requested page from the API is the Qunit module and has no content
					if (
						resp.query.pages[ resp.query.pageids[ 0 ] ].revisions &&
						resp.query.pages[ resp.query.pageids[ 0 ] ].revisions[ 0 ]
					) {
						pageContent = resp.query.pages[ resp.query.pageids[ 0 ] ].revisions[ 0 ][ '*' ];
					}
					return sourceHandler.extractParametersFromTemplateCode( pageContent );
				},
				function () {
					// Resolve an empty parameters array
					return $.Deferred().resolve( [] );
				}
			);
		} else {
			// No template found. Resolve to empty array of parameters
			this.templateSourceCodePromise = $.Deferred().resolve( [] );
		}
	}

	return this.templateSourceCodePromise;
};

/**
 * Retrieve template parameters from the template code.
 *
 * Adapted from https://he.wikipedia.org/wiki/MediaWiki:Gadget-TemplateParamWizard.js
 *
 * @param {string} templateCode Source of the template.
 * @return {string[]} An array of parameters that appear in the template code
 */
SourceHandler.prototype.extractParametersFromTemplateCode = function ( templateCode ) {
	var paramNames = [],
		normalizedParamNames = [],
		// This regex matches the one in TemplateDataBlob.php
		paramExtractor = /{{{+([^\n#={|}]*?)([<|]|}}})/mg;

	// Ignore non-wikitext content in comments and wikitext-escaping tags
	templateCode = templateCode.replace( /<!--[\s\S]*?-->/g, '' )
		.replace( /<nowiki\s*>[\s\S]*?<\/nowiki\s*>/g, '' )
		.replace( /<pre\s*>[\s\S]*?<\/pre\s*>/g, '' );

	var matches;
	while ( ( matches = paramExtractor.exec( templateCode ) ) !== null ) {
		// This normalization process is repeated in PHP in TemplateDataBlob.php
		var normalizedParamName = matches[ 1 ].replace( /[-_ ]+/, ' ' ).trim().toLowerCase();
		if ( !normalizedParamName || normalizedParamNames.indexOf( normalizedParamName ) !== -1 ) {
			continue;
		}
		if ( paramNames.indexOf( matches[ 1 ] ) === -1 ) {
			normalizedParamNames.push( normalizedParamName );
			paramNames.push( matches[ 1 ].trim() );
		}
	}

	return paramNames;
};

/**
 * Look for a templatedata json string and return it, if it exists.
 *
 * @param {string} templateDataString Wikitext templatedata string
 * @return {string|null} The isolated json string. Empty if no
 * templatedata string was found.
 */
SourceHandler.prototype.findModelInString = function ( templateDataString ) {
	var parts = templateDataString.match(
		/<templatedata>([\s\S]*?)<\/templatedata>/i
	);

	if ( parts && parts[ 1 ] && parts[ 1 ].trim().length > 0 ) {
		return parts[ 1 ].trim();
	} else {
		return null;
	}
};

/**
 * Set the page as a sub page of the main template
 *
 * @param {boolean} isSubLevel Page is sublevel
 */
SourceHandler.prototype.setPageSubLevel = function ( isSubLevel ) {
	this.subLevel = !!isSubLevel;
};

/**
 * Set the page as a sub page of the main template
 *
 * @return {boolean} Page is sublevel
 */
SourceHandler.prototype.isPageSubLevel = function () {
	return this.subLevel;
};

/**
 * Get full page name
 *
 * @param {string} pageName Page name
 */
SourceHandler.prototype.setFullPageName = function ( pageName ) {
	this.fullPageName = pageName || '';
};

/**
 * Get page full name
 *
 * @return {string} Page full name
 */
SourceHandler.prototype.getFullPageName = function () {
	return this.fullPageName;
};

/**
 * Set parent page
 *
 * @param {string} parent Parent page
 */
SourceHandler.prototype.setParentPage = function ( parent ) {
	this.parentPage = parent || '';
};

/**
 * Get parent page
 *
 * @return {string} Parent page
 */
SourceHandler.prototype.getParentPage = function () {
	return this.parentPage;
};

/**
 * Set template source code parameters
 *
 * @param {string[]} params Parameters from the template source code
 */
SourceHandler.prototype.setTemplateSourceCodeParams = function ( params ) {
	this.templateSourceCodeParams = params;
};

/**
 * Set template source code parameters
 *
 * @return {string[]} Parameters from the template source code
 */
SourceHandler.prototype.getTemplateSourceCodeParams = function () {
	return this.templateSourceCodeParams;
};

module.exports = SourceHandler;