跳转到内容

User:Vanished user 1231123/rater.js

维基百科,自由的百科全书
(重定向自User:佛壁灯/rater.js
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/***************************************************************************************************
 Rater --- by Evad37
 > Helps assess WikiProject banners.
***************************************************************************************************/
// <nowiki>
$( function($) {
/* ========== Config ============================================================================ */
// A global object that stores all the page and user configuration and settings
var config = {};
// Script info
config.script = {
	// Advert to append to edit summaries
	advert:  ' ([[en:User:Evad37/rater.js|Rater]])',
	version: '1.2.2'
};
// MediaWiki configuration values
config.mw = mw.config.get( [
	'skin',
	'wgPageName',
	'wgNamespaceNumber',
	'wgUserName',
	'wgFormattedNamespaces',
	'wgMonthNames',
	'wgRevisionId',
	'wgScriptPath',
	'wgServer',
	'wgCategories',
	'wgIsMainPage'
] );
// Do not operate on Special: pages, nor on non-existent pages or their talk pages
if ( config.mw.wgNamespaceNumber < 0 || $('li.new[id|=ca-nstab]').length ) {
	return;
}
// For User and User_talk namespaces, only operate on subpages
if (
	config.mw.wgNamespaceNumber >= 2 &&
	config.mw.wgNamespaceNumber <= 3 &&
	config.mw.wgPageName.indexOf('/') === -1
) {
	return;
}
config.regex = {
	// Pattern to find templates, which may contain other templates
	template:		/\{\{\s*([^#\{\s].+?)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/g,
	// Pattern to find `|param=value` or `|value`, where `value` can only contain a pipe
	// if within square brackets (i.e. wikilinks) or braces (i.e. templates)
	templateParams:	/\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g
};
config.deferred = {};
config.bannerDefaults = {
	classes: [
		'FA',
		'FL',
		'A',
		'GA',
		'B',
		'C',
		'Start',
		'Stub',
		'List'
	],
	importances: [
		'Top',
		'High',
		'Mid',
		'Low'
	],
	extendedClasses: [
		'Category',
		'Draft',
		'File',
		'Portal',
		'Project',
		'Template',
		'Bplus',
		'Future',
		'Current',
		'Disambig',
		'NA',
		'Redirect',
		'Book'
	],
	extendedImportances: [
		'Top',
		'High',
		'Mid',
		'Low',
		'Bottom',
		'NA'
	]
};
config.customClasses = {
	'WikiProject Military history': [
		'AL',
		'BL',
		'CL'
	],
	'WikiProject Portals': [
		'FPo',
		'Complete',
		'Substantial',
		'Basic',
		'Incomplete',
		'Meta'
	]
};
config.shellTemplates = [
	'WikiProject banner shell',
	'WikiProjectBanners',
	'WikiProject Banners',
	'WPB',
	'WPBS',
	'Wikiprojectbannershell',
	'WikiProject Banner Shell',
	'Wpb',
	'WPBannerShell',
	'Wpbs',
	'Wikiprojectbanners',
	'WP Banner Shell',
	'WP banner shell',
	'Bannershell',
	'Wikiproject banner shell',
	'WikiProject Banners Shell',
	'WikiProjectBanner Shell',
	'WikiProjectBannerShell',
	'WikiProject BannerShell',
	'WikiprojectBannerShell',
	'WikiProject banner shell/redirect',
	'WikiProject Shell',
	'Banner shell',
	'Scope shell',
	'Project shell',
	'WikiProject banner'
];
config.defaultParameterData = {
	"auto": {
		"label": {
			"en": "Auto-rated"
		},
		"description": {
			"en": "Automatically rated by a bot. Allowed values: ['yes']."
		},
		"autovalue": "yes"
	},
	"listas": {
		"label": {
			"en": "List as"
		},
		"description": {
			"en": "Sortkey for talk page"
		}
	},
	"small": {
		"label": {
			"en": "Small?",
		},
		"description": {
			"en": "Display a small version. Allowed values: ['yes']."
		},
		"autovalue": "yes"
	},
	"attention": {
		"label": {
			"en": "Attention required?",
		},
		"description": {
			"en": "Immediate attention required. Allowed values: ['yes']."
		},
		"autovalue": "yes"
	},
	"needs-image": {
		"label": {
			"en": "Needs image?",
		},
		"description": {
			"en": "Request that an image or photograph of the subject be added to the article. Allowed values: ['yes']."
		},
		"aliases": [
			"needs-photo"
		],
		"autovalue": "yes",
		"suggested": true
	},
	"needs-infobox": {
		"label": {
			"en": "Needs infobox?",
		},
		"description": {
			"en": "Request that an infobox be added to the article. Allowed values: ['yes']."
		},
		"aliases": [
			"needs-photo"
		],
		"autovalue": "yes",
		"suggested": true
	}
};

/* ========== Load dependencies ================================================================= */
// Load Morebits gadget if not already available
if ( window.Morebits == null ) {
	importScript('MediaWiki:Gadget-morebits.js');
	importStylesheet( 'MediaWiki:Gadget-morebits.css' );
}
// Load extra.js if not already available
if ( window.extraJs == null ) {
	importScript('User:Evad37/extra.js');
}
// Load resoucre loader modules
mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.util',
	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui'], function () {

/* ========== CSS =============================================================================== */
// TODO: convert to .css subpage and load using importStylesheet()
// Attribution: Diff styles from <https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/style.css>
mw.util.addCSS(
	/* ----- Main dialog styles --------------------------------------------------------------- */
	'#dialog-loading .oo-ui-progressBarWidget, #dialog-loading .oo-ui-progressBarWidget > div { max-width: 100%; height: 1.5em; }'+
	'.rater-dialog-row { padding:0.2em; border-bottom: 1px solid #777; }'+
	'.rater-dialog-row:nth-child(even) { background-color:#e0e0e0; }'+
	'.rater-dialog-row > div:nth-child(2) { clear:left; }'+
	'.rater-dialog-row > div > span { padding-right:0.5em; white-space:nowrap; }'+
	'.rater-dialog-para-label { cursor:help; padding-right:0; }'+
	'.rater-dialog-para-code { font-family:monospace; font-size:123%; padding-right:0; }'+
	'.rater-dialog-para-code::before { content:"|"; }'+
	'.rater-dialog-para-code::after { content:"="; }'+
	'.rater-dialog-dropdown { width:5em; margin:0 0.2em; }'+
	'.rater-dialog-textInputContainer input { width:6em; margin:0 0.15em; }'+
	'.rater-dialog-autofill { border:1px dashed #cd20ff; padding:0.2em; margin-right:0.2em; }'+
	'.rater-dialog-autofill::after { content:"autofilled"; color:#cd20ff; font-weight:bold; font-size:96%; }'+
	/* ----- OOjs UI windows ------------------------------------------------------------------ */
	// Need to be shown above Morebits SimpleWindow, which has z-index of ~1000
	'.rater-oouiWindowManager, .rater-oouiWindowManager > div { z-index:2000 !important; }'+
	'.oo-ui-window.oo-ui-dialog.oo-ui-messageDialog.oo-ui-window-active.oo-ui-window-setup.oo-ui-window-ready { z-index: 3000; }'+
	/* ----- Diff styles ---------------------------------------------------------------------- */
	'table.diff, td.diff-otitle, td.diff-ntitle { background-color: white; }'+
	'td.diff-otitle, td.diff-ntitle { text-align: center; }'+
	'td.diff-marker { text-align: right; font-weight: bold; font-size: 1.25em; }'+
	'td.diff-lineno { font-weight: bold; }'+
	'td.diff-addedline, td.diff-deletedline, td.diff-context { font-size: 88%; vertical-align: top; white-space: -moz-pre-wrap; white-space: pre-wrap; }'+
	'td.diff-addedline, td.diff-deletedline { border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; }'+
	'td.diff-addedline { border-color: #a3d3ff; }'+
	'td.diff-deletedline { border-color: #ffe49c; }'+
	'td.diff-context { background: #f3f3f3; color: #333333; border-style: solid; border-width: 1px 1px 1px 4px; border-color: #e6e6e6; border-radius: 0.33em; }'+
	'.diffchange { font-weight: bold; text-decoration: none; }'+
	'table.diff { border: none; width: 98%; border-spacing: 4px;'+
		/* Ensure that colums are of equal width: */ 'table-layout: fixed; }'+
	'td.diff-addedline .diffchange, td.diff-deletedline .diffchange { border-radius: 0.33em; padding: 0.25em 0; }'+
	'td.diff-addedline .diffchange {	background: #d8ecff; }'+
	'td.diff-deletedline .diffchange { background: #feeec8; }'+
	'table.diff td {	padding: 0.33em 0.66em; }'+
	'table.diff col.diff-marker { width: 2%; }'+
	'table.diff col.diff-content { width: 48%; }'+
	'table.diff td div {'+
		/* Force-wrap very long lines such as URLs or page-widening char strings. */
		'word-wrap: break-word;'+
		/* As fallback (FF<3.5, Opera <10.5), scrollbars will be added for very wide cells
		   instead of text overflowing or widening */
		'overflow: auto;'+
	'}'
);

/* ========== Utility functions ================================================================= */
/** writeToCache
 * @param {String} key
 * @param {Array|Object} val
 * @param {Number} staleDays Number of days after which the data becomes stale (usable, but should
 *  be updated for next time).
 * @param {Number} expiryDays Number of days after which the cached data may be deleted.
 */
var writeToCache = function(key, val, staleDays, expiryDays) {
	try {
        var defaultStaleDays = 1;
		var defaultExpiryDays = 30;
		var millisecondsPerDay = 24*60*60*1000;

        var staleDuration = (staleDays || defaultStaleDays)*millisecondsPerDay;
		var expiryDuration = (expiryDays || defaultExpiryDays)*millisecondsPerDay;
		
		var stringVal = JSON.stringify({
			value: val,
			staleDate: new Date(Date.now() + staleDuration).toISOString(),
            expiryDate: new Date(Date.now() + expiryDuration).toISOString()
		});
		localStorage.setItem('Rater-'+key, stringVal);
	}  catch(e) {}
};
/** readFromCache
 * @param {String} key
 * @returns {Array|Object|String|Null} Cached array or object, or empty string if not yet cached,
 *          or null if there was error.
 */
var readFromCache = function(key) {
	var val;
	try {
		var stringVal = localStorage.getItem('Rater-'+key);
		if ( stringVal !== '' ) {
            val = JSON.parse(stringVal);
		}
	}  catch(e) {
		console.log('[Rater] error reading ' + key + ' from localStorage cache:');
		console.log(
			'\t' + e.name + ' message: ' + e.message +
			( e.at ? ' at: ' + e.at : '') +
			( e.text ? ' text: ' + e.text : '')
		);
	}
	return val || null;
};
var isAfterDate = function(dateString) {
    return new Date(dateString) < new Date();
};
var clearCacheItemIfInvalid = function(key) {
    var isRaterKey = key.indexOf('Rater-') === 0;
    if ( !isRaterKey ) {
        return;
    }
    var item = readFromCache(key.replace('Rater-',''));
    var isInvalid = !item || !item.expiryDate || isAfterDate(item.expiryDate);
    if ( isInvalid ) {
        localStorage.removeItem(key);
    }
};
var clearInvalidCacheItems = function() {
    for (var i = 0; i < localStorage.length; i++) {
        setTimeout(clearCacheItemIfInvalid, 100, localStorage.key(i));
    }
};
	
/* ========== API =============================================================================== */
var API = new mw.Api( {
    ajax: {
        headers: { 
			'Api-User-Agent': 'Rater/' + config.script.version + 
				' ( https://en.wikipedia.org/wiki/User:Evad37/Rater )'
		}
    }
} );
/* ---------- API for ORES ---------------------------------------------------------------------- */
API.getORES = function(revisionID) {
	return $.get('https://ores.wikimedia.org/v3/scores/enwiki?models=wp10&revids='+revisionID);
};
/* ---------- Raw wikitext ---------------------------------------------------------------------- */
API.getRaw = function(page) {
	var gotRaw = $.Deferred();
	
	var request = $.get('https:' + config.mw.wgServer + mw.util.getUrl(page, {action:'raw'}))
	.done(function(data) {
		if ( !data ) {
			gotRaw.reject('ok-but-empty');
			return;
		}
		gotRaw.resolve(data);
	})
	.fail(function(){
		status = request.getResponseHeader('status');
		gotRaw.reject('http', {textstatus: status || 'unknown'});
	});
	
	return gotRaw;
};

/* ========== Additional config & set up ======================================================== */
// Get list of banners
var getListOfBannersFromApi = function() {

	var finishedPromise = $.Deferred();

	var querySkeleton = {
		action: 'query',
		format: 'json',
		list: 'categorymembers',
		cmprop: 'title',
		cmnamespace: '10',
		cmlimit: '500'
	};

	var categories = [
		{
			title:' Category:WikiProject banners with quality assessment',
			abbreviation: 'withRatings',
			banners: [],
			processed: $.Deferred()
		},
		{
			title: 'Category:WikiProject banners without quality assessment',
			abbreviation: 'withoutRatings',
			banners: [],
			processed: $.Deferred()
		},
		{
			title: 'Category:WikiProject banner wrapper templates',
			abbreviation: 'wrappers',
			banners: [],
			processed: $.Deferred()
		}
	];

	var processQuery = function(result, catIndex) {
		if ( !result.query || !result.query.categorymembers ) {
			// No results
			// TODO: error or warning ********
			finishedPromise.reject();
			return;
		}
		
		// Gather titles into array - excluding "Template:" prefix
		var resultTitles = result.query.categorymembers.map(function(info) {
			return info.title.slice(9);
		});
		Array.prototype.push.apply(categories[catIndex].banners, resultTitles);
		
		// Continue query if needed
		if ( result.continue ) {
			doApiQuery($.extend(categories[catIndex].query, result.continue), catIndex);
			return;
		}
		
		categories[catIndex].processed.resolve();
	};

	var doApiQuery = function(q, catIndex) {
		API.get( q )
		.done( function(result) {
			processQuery(result, catIndex);
		} )
		.fail( function(code, jqxhr) {
			console.warn('[Rater] ' + extraJs.makeErrorMsg(code, jqxhr, 'Could not retrieve pages from [[:' + q.cmtitle + ']]'));
			finishedPromise.reject();
		} );
	};
	
	categories.forEach(function(cat, index, arr) {
		cat.query = $.extend( { 'cmtitle':cat.title }, querySkeleton );
		$.when( arr[index-1] && arr[index-1].processed || true ).then(function(){
			doApiQuery(cat.query, index);
		});
	});
	
	categories[categories.length-1].processed.then(function(){
		var banners = {};
		var stashBanner = function(catObject) {
			banners[catObject.abbreviation] = catObject.banners;
		};
		var mergeBanners = function(mergeIntoThisArray, catObject) {
			return $.merge(mergeIntoThisArray, catObject.banners);
		};
		var makeOption = function(bannerName) {
			var isWrapper = ( -1 !== $.inArray(bannerName, categories[2].banners) );
			return {
				data:  ( isWrapper ? 'subst:' : '') + bannerName,
				label: bannerName.replace('WikiProject ', '') + ( isWrapper ? ' [template wrapper]' : '')
			};
		};
		categories.forEach(stashBanner);
		
		var bannerOptions = categories.reduce(mergeBanners, []).map(makeOption);
		
		config.banners = banners;
		config.bannerOptions = bannerOptions;
		writeToCache('banners', banners, 2, 60);
		writeToCache('bannerOptions', bannerOptions, 2, 60);
		finishedPromise.resolve();
	});
	
	return finishedPromise;
};
var getBannersFromCache = function() {
	var cachedBanners = readFromCache('banners');
	var cachedBannerOptions = readFromCache('bannerOptions');
	if (
		!cachedBanners ||
		!cachedBanners.value || !cachedBanners.staleDate ||
		!cachedBannerOptions ||
		!cachedBannerOptions.value || !cachedBannerOptions.staleDate
	) {
		return $.Deferred().reject();
	}
	if ( isAfterDate(cachedBanners.staleDate) || isAfterDate(cachedBannerOptions.staleDate) ) {
		// Update in the background; still use old list until then  
		getListOfBannersFromApi();
	}
	return $.Deferred().resolve(cachedBanners.value, cachedBannerOptions.value);
};
var setBannersAndBannerOptions = function(banners, bannerOptions) { 
	config.banners = banners;
	config.bannerOptions = bannerOptions;
	return true;
};

config.banners = {};
config.gotListOfBanners = getBannersFromCache().then(
	setBannersAndBannerOptions,
	getListOfBannersFromApi
);


/* ========== Page class ======================================================================== */
// Extended version of mw.Title <https://doc.wikimedia.org/mediawiki-core/master/js/#!/zhwiki/api/mw.Title>
/**
 * @class Page
 * @constructor
 * @param {string} title
 *  Title of the page (can be URI encoded)
 * @throws {Error} When the title is invalid
 */
var Page = function(title) {
	try {
		mw.Title.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse title "'+title+'"'); 
	}
	this.talk = null;
	this.subject = null;
	this.banners = [];
};
/**
 * Page.newFromText
 * Constructor with a null return instead of an exception for invalid titles.
 *
 * @static
 * @param {string} t
 *  Title of the page
 * @return {Page|null} A valid Page object or null if the title is invalid
 */
Page.newFromText = function(t) {
	if ( mw.Title.newFromText(t) ) {
		return new Page(t);
	} else {
		return null;
	}
};
// ---------- Page prototype -------------------------------------------------------------------- */
// Inherit from mw.Title
Page.prototype = Object.create(mw.Title.prototype);
Page.prototype.constructor = Page;
// Additional functions
/**
 * getTalk
 *
 * Get the name of the page's talk page (for subject-space pages) or the page itself (for
 * talk-space pages)
 *
 * @return {string} Talk page name, includng namespace prefix
 */
Page.prototype.getTalk = function() {
	if ( this.talk === null ) {
		// talk page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 1 ) {
			// Page is itself a talk page
			this.talk = this.getPrefixedText();
		} else {
			this.talk = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()+1
			).getPrefixedText();
		}
	}
	return this.talk;
};
/**
 * getSubject
 *
 * Get the name of the page's subject page (for talk-space pages) or the page itself (for
 * subject-space pages)
 *
 * @return {string} Subject page name, includng namespace prefix
 */
Page.prototype.getSubject = function() {
	if ( this.subject === null ) {
		// subject page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 0 ) {
			// Page is itself a subject page
			this.subject = this.getPrefixedText();
		} else {
			this.subject = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()-1
			).getPrefixedText();
		}
	}
	return this.subject;
};
/**
 * getListasAutofill
 *
 * Get the autofill value for the "listas" parameter ('Last, First Middle+', no disambiguation) 
 *
 * @return {string} autofill value for "listas" parameter
 */
Page.prototype.getListasAutofill = function() {
	var name = this.getMainText().replace(/\s\(.*\)/, '');
	if ( name.indexOf(' ') === -1 ) {
		return name;
	}
	var generationalSuffix = '';
	if ( / (?:[JS]r.?|[IVX]+)$/.test(name) ) {
		generationalSuffix = name.slice(name.lastIndexOf(' '));
		name = name.slice(0, name.lastIndexOf(' '));
		if ( name.indexOf(' ') === -1 ) {
			return name + generationalSuffix;
		}
	}
	var lastName = name.slice(name.lastIndexOf(' ')+1).replace(/,$/, '');
	var otherNames = name.slice(0, name.lastIndexOf(' '));
	return lastName + ', ' + otherNames + generationalSuffix;
};
/**
 * getRedirectOrPrefixedText
 *
 * Get the page name of this page's redirect target (if applicable), or of this page itself
 *
 * @return {string} page name, with namespace prefix
 */	
Page.prototype.getRedirectOrPrefixedText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getPrefixedText() : this.getPrefixedText();
};
/**
 * getRedirectOrMainText
 *
 * Get the page name, without the namespace prefix, of this page's redirect target (if applicable),
 * or of this page itself
 *
 * @return {string} page name, without namespace prefix
 */	
Page.prototype.getRedirectOrMainText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getMainText() : this.getMainText();
};
/**
 * getBannerFromNameOrRedirect
 *
 * Get one of this page's banners (Template objects) from either its page name, or from the name of
 * the page it redirects to.
 *
 * @param {string} bannerNameOrRedirect
 * @return {Template|boolean} Template object if found, or `false` if not found
 */	
Page.prototype.getBannerFromNameOrRedirect = function(bannerNameOrRedirect) {
	if ( !this.banners ) {
		return false;
	}
	
	var toCheckTitleObject = mw.Title.newFromText('Template:'+bannerNameOrRedirect);
	var toCheckPrefixedText = toCheckTitleObject && toCheckTitleObject.getPrefixedText();
	
	for ( var i=0; i<this.banners.length; i++ ) {
		if (
			this.banners[i].getPrefixedText() === toCheckPrefixedText ||
			this.banners[i].getRedirectOrPrefixedText() === toCheckPrefixedText
		) {
			return this.banners[i];
		}
	}
	return false;
};
/**
 * getTalkpageTopSection
 *
 * Retrieve the wikitext of the top section of the talk page, and store it as {this}.oldTopSection
 *
 * @return {jQuery.Deferred} Deferred object: resolved once oldTopSection is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getTalkpageTopSection = function() {
	var self = this;
	var gotTalkpageTopSection = $.Deferred();
	
	var processTalk = function (result) {
		var id = result.query.pageids;		
		self.oldTopSection = ( id < 0 ) ? '' : result.query.pages[id].revisions[0]['*'];
		gotTalkpageTopSection.resolve();
	};
	
	API.get( {
		action: 'query',
		prop: 'revisions',
		rvprop: 'content',
		rvsection: '0',
		titles: self.getTalk(),
		indexpageids: 1
	} )
	.done( processTalk )
	.fail( gotTalkpageTopSection.reject );	
	
	return gotTalkpageTopSection;
};
/**
 * getLatestSubjectRevisionID
 *
 * Retrieve the revision ID of subject page's latest revision, and store it as
 * {this}.latestSubjectRevisionID
 *
 * @return {jQuery.Deferred} Deferred object: resolved once latestSubjectRevisionID is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getLatestSubjectRevisionID = function() {
	
	var self = this;
	var gotRevisionID = $.Deferred();
	
	if  ( config.mw.wgNamespaceNumber === 0 && config.mw.wgRevisionId !== 0 ) {
		self.latestSubjectRevisionID = config.mw.wgRevisionId;
		return gotRevisionID.resolve();
	}
	
	var processRevision = function(result) {		
		var id = result.query.pageids;
		if ( id < 0 ) {
			gotRevisionID.reject();
			return;
		}
		self.latestSubjectRevisionID = result.query.pages[id].revisions[0].revid;
		gotRevisionID.resolve();
	};
	
	API.get( {
		action: 'query',
		format: 'json',
		prop: 'revisions',
		titles: self.getSubject(),
		rvprop: 'ids',
		indexpageids: 1
	} )
	.done( processRevision )
	.fail( gotRevisionID.reject );	
	
	return gotRevisionID;	
};
/**
 * getOresScore
 *
 * Retrieve the ORES score of subject page's latest revision, and store it as
 * {this}.oresScore
 *
 * @return {jQuery.Deferred} Deferred object: resolved once oresScore is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getOresScore = function() {
	var self = this;
	var gotOresScore = $.Deferred();
	
	$.when( self.latestSubjectRevisionID || self.getLatestSubjectRevisionID() )
	.done( function() {
		API.getORES(self.latestSubjectRevisionID)
		.done(function(result) {
			var data = result.enwiki.scores[self.latestSubjectRevisionID].wp10;
			if ( data.error ) {
				gotOresScore.reject(data.error.type, data.error.message);
				return;
			}
			self.oresScore = data.score.prediction;
			gotOresScore.resolve();
		})
		.fail( gotOresScore.reject );
	})
	.fail( gotOresScore.reject );
	
	return gotOresScore;
};
/**
 * getSubjectRawWikitext
 *
 * Get the raw wikitext of subject page
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the raw wikitext, or is rejected with
 *  http error details if the request fails 
 */
Page.prototype.getSubjectRawWikitext = function() {
	return API.getRaw(this.getSubject());
};
/**
 * makeEdit
 *
 * Makes the edit to the talk page
 *
 * @return {jQuery.Deferred} Deferred object: resolved if the edit is done, or is rejected with
 *  Api error details if the request fails
 */
Page.prototype.makeEdit = function() {
	var self = this;
	var editMade = $.Deferred();
	
	API.postWithToken( 'csrf', {
		action: 'edit',
		title: self.getTalk(),
		text: self.makeNewTopSection(),
		section: 0,
		summary: self.makeEditSummary() + config.script.advert
	} )
	.done( editMade.resolve )
	.fail( editMade.reject );
	return editMade;
};
/**
 * makePreview
 *
 * Make HTML of a preview of the edit
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the preview HTML, or is rejected with
 *  Api error details if the request fails 
 */
Page.prototype.makePreview = function() {
	var self = this;
	var madePreview = $.Deferred();
	
	API.get({
		action: 'parse',
		contentmodel: 'wikitext',
		text: self.makeNewTopSection(),
		title: self.getTalk(),
		pst: 1
	})
	.done(function(result) {
		if ( !result || !result.parse || !result.parse.text || !result.parse.text['*'] ){
			madePreview.reject('Empty result');
		}
		madePreview.resolve(result.parse.text['*']);
	})
	.fail(madePreview.reject);
	
	return madePreview;
};
/**
 * makeDiff
 *
 * Make HTML of a diff to the current wikitext.
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the diff HTML, or is rejected with
 *  Api error details if the request fails 
 */
Page.prototype.makeDiff = function() {
	var self = this;
	var madeDiff = $.Deferred();
	
	API.get({
		action: "compare",
		format: "json",
		fromtext: self.oldTopSection,
		fromcontentmodel: "wikitext",
		totext: self.makeNewTopSection(),
		tocontentmodel: "wikitext",
		prop: "diff"
	})
	.done(function(result) {
		if ( !result || !result.compare || !result.compare['*'] ){
			madeDiff.reject('Empty result');
			return;
		}
		var diffTable = $('<table>').css('width', '100%').append(
			$('<tr>').append(
				$('<th>').attr({'colspan':'2', 'scope':'col'}).css('width', '50%').text('Latest revision'),
				$('<th>').attr({'colspan':'2', 'scope':'col'}).css('width', '50%').text('New text')
			),
			result.compare['*']
		);
		madeDiff.resolve(diffTable);
	})
	.fail(madeDiff.reject);
	
	return madeDiff;
};
/**
 * splitOldTopSection
 *
 * Splits the oldTopSection wikitext into three parts: project banners, and wikitext above and below
 *
 * @return {object} with keys 'above', 'projects', 'below', each of which is a string.
 */
Page.prototype.splitOldTopSection = function() {
	var self = this;
	
	var wikitext = {
		above: '',
		projects: '',
		below: ''
	};

	if ( self.oldTopSection && self.banners ) {
		var firstBannerIndex = self.banners.reduce(function(currentMinIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMinIndex;
			}
			return Math.min(
				currentMinIndex,
				self.oldTopSection.indexOf(banner.rawWikitext)
			);
		}, Infinity);
		var afterLastBannerIndex = self.banners.reduce(function(currentMaxIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMaxIndex;
			}
			return Math.max(
				currentMaxIndex,
				self.oldTopSection.indexOf(banner.rawWikitext) + banner.rawWikitext.length
			);
		}, -1);
		
		if ( firstBannerIndex >= afterLastBannerIndex ) {
			// TODO: ask for user confirmation?
			// Place new banners at end of top section
			wikitext.above = self.oldTopSection;
		} else {
			wikitext.above = self.oldTopSection.slice(0, firstBannerIndex).trim();
			wikitext.projects = self.oldTopSection.slice(firstBannerIndex, afterLastBannerIndex).trim();
			wikitext.below = self.oldTopSection.slice(afterLastBannerIndex).trim();
		}
		
	}
	
	return wikitext;
};
/**
 * makeBannerShellWikitext
 *
 * Make wikitext for wikiproject banner shell template (that wraps around project banners).
 *
 * @return {object|false} Wikitext for banner shell -- stored in keys `top` and `bottom`, for the 
 *  wikitext that goes above and below the project banners -- or `false` if banner shell shouldn't
 *  be added.
 */
Page.prototype.makeBannerShellWikitext = function() {
	var self = this;

	var shell = {
		top: '{{WikiProject banner shell',
		bottom: '}}'
	};
	
	if ( self.hasShellTemplate ) {
		return false;
	}
	
	var bannersPresent = self.banners.filter(function(banner) {
		return !banner.remove;
	});
	if ( bannersPresent.length < 3 ) {
		return false;
	}
	
	var biographyBanner = self.getBannerFromNameOrRedirect('WikiProject Biography');
	if ( !biographyBanner ) {
		shell.top += '|1=';
		return shell;
	}
	
	var isYes = function(paramValue) {
		return /^(yes|y|true|1)$/i.test((paramValue || '').trim());
	};
	if (
		isYes(biographyBanner.getParamValue('living')) ||
		isYes(biographyBanner.getParamValue('blp')) ||
		isYes(biographyBanner.getParamValue('BLP'))
	) {
		shell.top += '|living=yes';
		if ( isYes(biographyBanner.getParamValue('activepol')) ) {
			shell.top += '|activepol=yes';
		}
	} else if ( isYes(biographyBanner.getParamValue('blpo')) ) {
		shell.top += '|blpo=yes';
	}
	
	shell.top += '|1=';
	return shell;
};
/**
 * makeUpdatedBanners
 *
 * Make wikitext for the edited top section.
 *
 * @param {string} projects
 *  Existing wikitext of project banners
 * @return {string} Updated wikitext of project banners
 */
Page.prototype.makeUpdatedBanners = function(projects) {
	var self = this;
	
	for ( var i=0; i<self.banners.length; i++ ) {
		// Not touched (and not new, and not bypassed, and not removed)
		if ( $.isEmptyObject(self.banners[i].touched) && !self.banners[i].isNew() && !self.banners[i].bypassRedirect && !self.banners[i].remove ) {
			continue;
		}
		// Marked for removal
		if ( self.banners[i].remove ) {
			projects = projects.replace(self.banners[i].rawWikitext, '');
			continue;
		}
		// Existing banner that's been modified
		if ( !self.banners[i].isNew() ) {
			projects =  projects.replace(self.banners[i].rawWikitext.trim(), self.banners[i].buildWikitext());
			continue;
		}
		// New banner
		projects += '\n' + self.banners[i].buildWikitext();
	}
	return projects.trim();
};

/**
 * makeNewTopSection
 *
 * Make wikitext for the edited top section.
 *
 * @return {string} wikitext
 */
Page.prototype.makeNewTopSection = function() {
	var self = this;
	
	var wikitext = self.splitOldTopSection();
	
	var projects = self.makeUpdatedBanners(wikitext.projects);
	
	var shell = self.makeBannerShellWikitext();
	if ( shell ) {
		projects  = shell.top + '\n' + projects + '\n' + shell.bottom;
	}

	return (wikitext.above + '\n' + projects + '\n' + wikitext.below).trim();
};
/**
 * makeEditSummary
 *
 * Make the edit summary for the edit.
 *
 * @return {string} edit summary
 */
Page.prototype.makeEditSummary = function() {
	return this.banners.reduce(function(changes, banner) {
		// Not touched (and not new, and not bypassed, and not removed)
		if ( $.isEmptyObject(banner.touched) && !banner.isNew() && !banner.bypassRedirect && !banner.remove ) {
			return changes;
		}
		// New and removed, no action needed
		if ( banner.remove && banner.isNew() ) {
			return changes;
		}
		
		// Symbol
		var symbol = '';
		if ( banner.remove ) {
			symbol = '−';
		} else if ( banner.isNew() ) {
			symbol = '+';
		}
		// Transclusion name, without WikiProject prefix
		var name = banner.getTransclusionName().replace('WikiProject ','').replace('Subst:','');
		// Ratings, if touched
		var rating = '';
		if ( !banner.remove ) {
			var classRating = ( banner.touched.class ) ? banner.parameters.class.trim() : '';
			var impRating = ( banner.touched.importance ) ? banner.parameters.importance.trim() : '';
			if ( classRating && impRating ) {
				rating = classRating + '/' + impRating;
			} else {
				rating = classRating || impRating || '';
			}
			if ( rating ) {
				rating = ' (' + rating + ')';
			}
		}
		return changes += ' ' + symbol + name + rating + ';';
	}, 'Assessment:').slice(0,-1);
};
/**
 * setRedirectsTo
 *
 * If this Page is a redirect, set the redirect targart as {this}.redirectsTo
 *
 * @param {Page[]|null} Array of page objects to work on instead of this page
 * @return {jQuery.Deferred} Deferred object: resolved when all Pages have been processed, or is
 *  rejected with Api error details if the request fails 
 */
Page.prototype.setRedirectsTo = function(pageObjects) {
	var gotRedirects = $.Deferred();
	
	if ( pageObjects == null ) {
		pageObjects = this;
	}
	
	var pageObjectsToCheck = ( $.isArray(pageObjects) ) ? pageObjects : [pageObjects];

	var processRedirects = function(result) {
		if ( !result || !result.query ) {
			gotRedirects.reject();
			return;
		}
		if ( result.query.redirects ) {
			$.each(result.query.redirects, function(_index, redirect) {
				for ( var i=0; i<pageObjectsToCheck.length; i++ ) {
					if ( pageObjectsToCheck[i].getPrefixedText() === redirect.from ) {
						pageObjectsToCheck[i].redirectsTo = Page.newFromText(redirect.to);
						break;
					}
				}
			});
		}
		gotRedirects.resolve();
	};
	
	API.get({
		'action': 'query',
		'format': 'json',
		'titles': pageObjectsToCheck.map(function(pageObj) {
			return pageObj.getPrefixedText();
		}),
		'redirects': 1
	})
	.done( processRedirects )
	.fail( gotRedirects.reject );
	
	return gotRedirects;
};
/**
 * setTemplatesBanners
 *
 * Parses {this}.oldTopSection for banners, makes Template objects for each banner, and sets
 * {this}.banners to an Array of those Template objects. Will also #setRedirectsTo and
 * #setParamDataAndSuggestions for the Template objects.
 *
 * @return {jQuery.Deferred} Deferred object: resolved when all banners have been set processed, or
 *  is rejected if an Api request fails
 */
Page.prototype.setTemplatesBanners = function() {
	var self = this;
	var gotTemplatesBanners = $.Deferred();
	
	if ( self.oldTopSection === '' ) {
		return gotTemplatesBanners.resolve();
	}

	// Initially finds top-level templates only (not templates within template parameters)
	var makeTemplatesObjectsArray = function(wikitext) {
		// Reset lastindex of regex pattern so that test will work
		config.regex.template.lastIndex = 0;
		if ( !config.regex.template.test(wikitext) ) {
			return [];
		}
		return wikitext.match(config.regex.template).map(Template.newFromRawWikitext);
	};
	var talkpageTemplates = makeTemplatesObjectsArray(self.oldTopSection);
	if ( !talkpageTemplates.length ) {
		return gotTemplatesBanners.resolve();
	}
	
	// Find sub-templates within WikiProject banner shell	
	var isShellTemplate = function(templateObject) {
		return -1 !== $.inArray(templateObject.getMainText(), config.shellTemplates);
	};
	var shellTemplate = talkpageTemplates.filter(isShellTemplate)[0];
	if ( shellTemplate && shellTemplate.parameters['1'] ) {
		self.hasShellTemplate = true;
		var talkpageSubtemplates = makeTemplatesObjectsArray(shellTemplate.parameters['1']);
		// Merge subtemplates into main array
		$.merge(talkpageTemplates, talkpageSubtemplates);
	}

	// Check for redirects, then filter out non-banners
	$.when(self.setRedirectsTo(talkpageTemplates), config.gotListOfBanners)
	.done(function(){
		var talkpageBanners = talkpageTemplates.filter(function(templateObject) {
			var redirectOrMainText = templateObject.getRedirectOrMainText();
			return  -1 !== $.inArray(redirectOrMainText, config.banners.withRatings) ||
				-1 !== $.inArray(redirectOrMainText, config.banners.withoutRatings) ||
				-1 !== $.inArray(redirectOrMainText, config.banners.wrappers);
		});
		
		self.banners = talkpageBanners;
		
		// Set wrapper target
		self.banners.forEach(function(templateObject) { templateObject.setWrapperTarget(); } );
		
		// Retrieve TemplateData
		var retrieveTemplateDatas = self.banners.map(function(templateObject) {
			return templateObject.setParamDataAndSuggestions()
			.fail(function(code, jqxhr){
				console.log('[Rater] Failed to retrieve TemplateData for ' + templateObject.getPrefixedText() +
				( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr));
			});
		});
		// Always resolve, because we can still work without the TemplateData
		$.when.apply(null, retrieveTemplateDatas).always(function() {
			gotTemplatesBanners.resolve();
		});
		
	})
	.fail(function() { gotTemplatesBanners.reject(); });
	return gotTemplatesBanners;
};

/* ========== Template class ==================================================================== */
// Extended version of Page class for banner templates
/**
 * @class Template
 * @constructor
 * @param {string} title
 *  Title of the template, including namespace prefix
 * @param {object|null} parameters
 *  Object of key:val pairs for parameter names (keys) and their values (vals)
 *  Title of the page (can be URI encoded)
 * @param {string|null} rawWikitext
 *  Wikitext the object was derived from, e.g. "{{Templatename|para1=val1|para2=val2}}"
 * @throws {Error} When the title is invalid
 */
var Template = function(title, parameters, rawWikitext) {
	try {
		Page.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse template title "'+title+'"'); 
	}
	this.parameters = parameters || {};
	this.rawWikitext = rawWikitext || null;
	this.isProjectBanner = null;
	this.touched = {};
	this.subst = /subst\:/i.test(title);
};
/**
 * Template.makeParamsObject
 *
 * Converts a string containg template parameters into an object of parameter names (or positons)
 * and their values.
 *
 * @static
 * @param {string} wikitext
 *  Wikitext containg parameters
 * @return {object} Object with key:value pairs correspoinding to |parameter=value pairs.
 */
Template.makeParamsObject = function(wikitext) {
	var params = {};
	var unnamedParamCount = 0;
	var parts = wikitext.match(config.regex.templateParams);
	for ( var i=0; i<parts.length; i++ ) {
		if ( parts[i].trim() === '|' ) {
			//Empty unnamed parameter, i.e. {{foo||bar}}
			unnamedParamCount++;
			continue;
		}
		var equalsIndex = parts[i].indexOf('=');
		var bracesIndex = parts[i].indexOf('{{');
		if (
			equalsIndex === -1 ||
			( -1 < bracesIndex && bracesIndex < equalsIndex )
		) {
			//unnamed parameter
			unnamedParamCount++;
			params[unnamedParamCount.toString()] = parts[i].slice(1).trim();
		} else {
			params[parts[i].slice(1, equalsIndex).trim()] = parts[i]
				.slice(equalsIndex+1).trim();
		}
	}
	return params;
};
/**
 * Template.newFromRawWikitext
 *
 * Constructor from raw wikitext as used on a wiki page.
 *
 * @static
 * @param {string} rawWikitext
 *  Raw wikitext of the template, e.g. "{{TemplateName|value1|para2=value2|para3=value3}}"
 * @return {Template|null} A valid Template object or null if the title is invalid
 * @throws {Error} If unable to parse the Raw wikitext
 */
// Constructor from raw wikitext, i.e. 
Template.newFromRawWikitext = function(rawWikitext) {
	// Reset lastindex of regex pattern so that exec will work
	config.regex.template.lastIndex = 0;
	var parts = config.regex.template.exec(rawWikitext);
	if ( !parts || !parts[0] || !parts[1] ) {
		throw new Error('Unable to parse template from wikitext: ' + rawWikitext);
	}
	var params = ( parts[2] ) ? Template.makeParamsObject(parts[2]) : null;
	return new Template('Template:'+parts[1], params, parts[0]);
};
// ---------- Template prototype ---------------------------------------------------------------- */
// Inherited from Page
Template.prototype = Object.create(Page.prototype);
Template.prototype.constructor = Template;
// Additional functions
/**
 * isNew
 *
 * Check if template is new, i.e. wasn't added from raw wikitext
 *
 * @return {boolean}
 */
Template.prototype.isNew = function() {
	return this.rawWikitext === null;
};
/**
 * getParamValue
 *
 * Get the currently stored value of a parameter
 *
 * @param {string} param
 *  Name of parameter
 * @return {string|null} Value of parameter, or null if not found
 */
Template.prototype.getParamValue = function(param) {
	return this.parameters[param] || null;
};
/**
 * setParamValue
 *
 * Set the currently stored value of a parameter
 *
 * @param {string} param
 *  Name of parameter
 * @param {string} val
 *  Value to set
 */
Template.prototype.setParamValue = function(param, val, setWithoutTouching) {
	this.parameters[param] = val;
	if ( !setWithoutTouching ) {
		this.touched[param] = true;
	}
};
/**
 * deleteParam
 *
 * Delete a parameter
 *
 * @param {string} param
 *  Name of parameter
 */
Template.prototype.deleteParam = function(param) {
	delete this.parameters[param];
	this.touched[param] = true;
};
/**
 * getTransclusionName
 *
 * Get the name of the template, without namespace prefix; or if {this}.bypassRedirect is true, get
 * the name of this template's redirect target, without namespace prefix.
 *
 * @return {string} Transclusion name
 */
Template.prototype.getTransclusionName = function() {
	if ( this.bypassRedirect ) {
		return this.redirectsTo.getMainText();
	}
	return this.getMainText();
};
/**
 * getLinkedName
 *
 * Get a link to the #getTransclusionName
 *
 * @return {jQuery} <a> element
 */
Template.prototype.getLinkedName = function() {
	if ( this.bypassRedirect ) {
		return extraJs.makeLink(this.redirectsTo.getPrefixedText(), this.redirectsTo.getMainText());
	}
	if ( this.subst ) {
		return extraJs.makeLink(this.getPrefixedText().replace('Subst:', ''), this.getMainText());
	}
	return extraJs.makeLink(this.getPrefixedText(), this.getMainText());
};

// Banner-specific functions
Template.prototype.parseClassesAndImportances = function() {
	var self = this;
	var parsed = $.Deferred();
	
	if ( self.classes && self.importances ) {
		return parsed.resolve();
	}
	
	var cachedRatings = readFromCache(self.getMainText()+'-ratings');
	if (
        cachedRatings &&
        cachedRatings.value &&
        cachedRatings.staleDate &&
        cachedRatings.value.classes!=null &&
        cachedRatings.value.importances!=null
    ) {
		self.classes = cachedRatings.value.classes;
        self.importances = cachedRatings.value.importances;
        parsed.resolve();
		if ( !isAfterDate(cachedRatings.staleDate) ) {
			// Just use the cached data
			return parsed;
		} // else: Use the cache data for now, but also fetch new data from API
	}
	
	var wikitextToParse = '';	
	$.each(config.bannerDefaults.extendedClasses, function(index, classname) {
		wikitextToParse += '{{' + self.getMainText() + '|class=' + classname + '|importance=' +
		(config.bannerDefaults.extendedImportances[index] || '') + '}}/n';
	});
	
	API.get({
		action: 'parse',
		title: 'Talk:Sandbox',
		text: wikitextToParse,
		prop: 'categorieshtml'
	})
	.then(function(result) {
		var catsHtml = result.parse.categorieshtml['*'];
		var extendedClasses = config.bannerDefaults.extendedClasses.filter(function(cl) {
			return catsHtml.indexOf(cl+'-Class') !== -1;
		});
		var defaultClasses = ( self.getRedirectOrMainText() === 'WikiProject Portals' )
			? ['List']
			: config.bannerDefaults.classes;
		var customClasses = config.customClasses[self.getRedirectOrMainText()] || [];
		self.classes = [].concat(
			customClasses,
			defaultClasses,
			extendedClasses
		);

		self.importances = config.bannerDefaults.extendedImportances.filter(function(imp) {
			return catsHtml.indexOf(imp+'-importance') !== -1;
		});
        writeToCache(self.getMainText()+'-ratings',
            {
                classes: self.classes,
                importances: self.importances
            },
            1
        );
		return true;
	})
	.then(
		parsed.resolve,
		parsed.reject
	);
	
	return parsed;
};
Template.prototype.getDataForParam = function(key, paraName) {
	if ( !this.paramData ) {
		return null;
	}
	// If alias, switch from alias to preferred parameter name
	para = this.paramAliases[paraName] || paraName;	
	if ( !this.paramData[para] ) {
		return;
	}
	
	var data = this.paramData[para][key];
	// Data might actually be an object with key "en"
	if ( data && data.en && !$.isArray(data) ) {
		return data.en;
	}
	return data;
};

Template.prototype.setParamDataAndSuggestions = function() {
	var self = this;
	var paramDataSet = $.Deferred();
	
	if ( self.paramData ) { return paramDataSet.resolve(); }
	
    var cachedInfo = readFromCache(self.getRedirectOrPrefixedText() + '-params');
    
	if (
        cachedInfo &&
        cachedInfo.value &&
        cachedInfo.staleDate &&
        cachedInfo.value.paramData != null &&
        cachedInfo.value.parameterSuggestions != null &&
        cachedInfo.value.paramAliases != null
    ) {
		self.notemplatedata = cachedInfo.value.notemplatedata;
		self.paramData = cachedInfo.value.paramData;
		self.parameterSuggestions = cachedInfo.value.parameterSuggestions;
        self.paramAliases = cachedInfo.value.paramAliases;
        
        paramDataSet.resolve();
		if ( !isAfterDate(cachedInfo.staleDate) ) {
			// Just use the cached data
			return paramDataSet;
		} // else: Use the cache data for now, but also fetch new data from API
	}
	
	API.get({
		action: 'templatedata',
		titles: self.getRedirectOrPrefixedText(),
		redirects: 1,
		doNotIgnoreMissingTitles: 1,
	})
	.then(
		function(response) { return response; },
		function(_error) { return null; }
	)
	.then( function(result) {
		// Figure out page id (beacuse action=templatedata doesn't have an indexpageids option)
		var id = result && $.map(result.pages, function( _value, key ) { return key; });
		
		if ( !result || !result.pages[id] || result.pages[id].notemplatedata || !result.pages[id].params ) {
			// No TemplateData, so use defaults (guesses)
			self.notemplatedata = true;
			self.templatedataApiError = !result;
			self.paramData = config.defaultParameterData;
		} else {
			self.paramData = result.pages[id].params;
		}
		self.paramAliases = {};
		
		var extractExtraParamData = function(paraName, paraData) {
			// Extract aliases for easier reference later on
			if ( paraData.aliases && paraData.aliases.length ) {
				paraData.aliases.forEach(function(alias){
					self.paramAliases[alias] = paraName;
				});
			}
			// Extract allowed values array from description
			if ( paraData.description && /\[.*'.+?'.*?\]/.test(paraData.description.en) ) {
				try {
					var allowedVals = JSON.parse(
						paraData.description.en
						.replace(/^.*\[/,'[')
						.replace(/"/g, '\\"')
						.replace(/'/g, '"')
						.replace(/,\s*]/, ']')
						.replace(/].*$/, ']')
					);
					self.paramData[paraName].allowedValues = allowedVals;
				} catch(e) {
					console.warn('[Rater] Could not parse allowed values in description:\n  '+
					paraData.description.en + '\n Check TemplateData for parameter |' + paraName +
					'= in ' + self.getPrefixedText());
				}
			}
			// Make sure required/suggested parameters are present
			if ( (paraData.required || paraData.suggested) && !self.parameters[paraName] ) {
				// Check aliases, if any
				if ( paraData.aliases.length ) {
					var makeParaObject = function(val, name) {
						return {'name': name, 'value':val};
					};
					var isNonEmptyAlias = function(paraObj) {
						var isAlias = (-1 !== $.inArray(paraObj.name, paraData.aliases));
						if ( !isAlias ) { return false; }
						var isEmpty = !self.parameters[paraObj.name];
						if ( isEmpty ) {
							self.deleteParam(paraObj.name);
							return false;
						}
						return true;
					};
					aliasesPresent = $.map(self.parameters, makeParaObject)
					.filter(isNonEmptyAlias);
					if ( aliasesPresent.length ) {
						// At least one non-empty alias, so do nothing
						return;
					}
				}
				// No non-empty aliases, so set parameter to either the autovaule, or
				// an empty string (without touching, unless it is a required parameter)
				self.setParamValue(paraName, paraData.autovalue || '', !paraData.required);
			}
		};
		$.each(self.paramData, extractExtraParamData);
		
		//Suggestions
		var allParamsArray = ( !self.notemplatedata && result.pages[id].paramOrder ) ||
			$.map(self.paramData, function(_val, key){
				return key;
			});
		self.parameterSuggestions = allParamsArray.filter(function(paramName) {
			return ( paramName !== 'class' && paramName !== 'importance' );
		})
		.map(function(paramName) {
			var optionObject = {data: paramName};
			var label = self.getDataForParam(label, paramName);
			if ( label ) {
				optionObject.label = label + ' (|' + paramName + '=)';
			}
			return optionObject;
		});
		
		if ( self.templatedataApiError ) {
			return true;
		}
		
		writeToCache(
            self.getRedirectOrPrefixedText() + '-params',
            {
                notemplatedata: self.notemplatedata,
                paramData: self.paramData,
                parameterSuggestions: self.parameterSuggestions,
                paramAliases: self.paramAliases
            },
            1
        );
		return true;
	})
	.then(
		paramDataSet.resolve,
		paramDataSet.reject
	);
	
	return paramDataSet;	
};

Template.prototype.getUnusedParamterSuggestions = function() {
	return this.parameterSuggestions || [];
	/*
	if ( !this.parameterSuggestions ) {
		return [];
	}
	
	return this.parameterSuggestions.filter(function(param) {
		return self.templateObject.parameters[param.data] == null;
	});
	*/
};

Template.prototype.buildWikitext = function() {
	var paras = '';
	if ( !$.isEmptyObject(this.parameters) ) {
		paras = $.map(this.parameters, function(val, name) {
			if ( val === null || val === '' ) { return '';}
			return ' |'+name+'='+val;
		}).join('');
	}		
	return '{{' + this.getTransclusionName() + paras + '}}';
};

Template.prototype.setWrapperTarget = function() {
	if ( -1 !== $.inArray(this.getRedirectOrMainText(), config.banners.wrappers) ) {
		this.redirectsTo = Page.newFromText('Template:Subst:' + this.getRedirectOrMainText());
	}
};

/* ========== OOjs UI =========================================================================== */
/* ---------- SuggestionLookupTextInputWidget --------------------------------------------------- */
var SuggestionLookupTextInputWidget = function( config ) {
	OO.ui.TextInputWidget.call( this, config );
	OO.ui.mixin.LookupElement.call( this, config );
	this.suggestions = config.suggestions || [];
};
OO.inheritClass( SuggestionLookupTextInputWidget, OO.ui.TextInputWidget );
OO.mixinClass( SuggestionLookupTextInputWidget, OO.ui.mixin.LookupElement );
// Set suggestion. param: Object[] with objects of the form { data: ... , label: ... }
SuggestionLookupTextInputWidget.prototype.setSuggestions = function(suggestions) {
	this.suggestions = suggestions;
};
// Returns data, as a resolution to a promise, to be passed to #getLookupMenuOptionsFromData
SuggestionLookupTextInputWidget.prototype.getLookupRequest = function () {
	var deferred = $.Deferred().resolve(new RegExp('\\b' + mw.RegExp.escape(this.getValue()), 'i'));
	return deferred.promise( { abort: function () {} } );
};
// ???
SuggestionLookupTextInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
	return response || [];
};
// Is passed data from #getLookupRequest, returns an array of menu item widgets 
SuggestionLookupTextInputWidget.prototype.getLookupMenuOptionsFromData = function ( pattern ) {
	var labelMatchesInputVal = function(suggestionItem) {
		return pattern.test(suggestionItem.label) || ( !suggestionItem.label && pattern.test(suggestionItem.data) );
	};
	var makeMenuOptionWidget = function(optionItem) {
		return new OO.ui.MenuOptionWidget( {
			data: optionItem.data,
			label: optionItem.label || optionItem.data
		} );
	};
	return this.suggestions.filter(labelMatchesInputVal).map(makeMenuOptionWidget);
};

/* ---------- ComboBoxInputPrompt --------------------------------------------------------------- */
// A prompt contining a text input, which has dropdown menu of suggestions 
var ComboBoxInputPrompt = function( config ) {
  ComboBoxInputPrompt.super.call( this, config );
};
OO.inheritClass( ComboBoxInputPrompt, OO.ui.ProcessDialog );
ComboBoxInputPrompt.static.name = 'comboBoxInput';
ComboBoxInputPrompt.static.actions = [
  { flags: 'primary', label: 'Add', action: 'add' },
  { flags: 'safe', label: 'Cancel' }
];
// Content and layout (that appear for every instance)
ComboBoxInputPrompt.prototype.initialize = function () {
	ComboBoxInputPrompt.super.prototype.initialize.call( this );
	this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.content = new OO.ui.FieldsetLayout();
	this.input = new SuggestionLookupTextInputWidget( {
		$overlay: this.$overlay/*,
		menu: {
			filterFromInput: true
		}*/
	});
	this.field = new OO.ui.FieldLayout( this.input, {
		label: ' ',//placeholder
		align: 'top'
	} );
	this.content.addItems([ this.field ]);
	this.panel.$element.append( this.content.$element );
	this.$body.append( this.panel.$element );
	this.input.connect( this, { 'change': 'onInputChange' } );
	this.input.connect( this, { 'enter': 'onEnterPress' } );
};

ComboBoxInputPrompt.prototype.onInputChange = function ( value ) {
    // Disable Add button when input value is empty
    this.actions.setAbilities( {
        add: !!value.length 
    } );
};
// Make pressing Enter key the same as cicking Add button
ComboBoxInputPrompt.prototype.onEnterPress = function() {
	this.executeAction('add');
};
// Dialog height is the height of the panel element plus a bit (instead of auto-generated height)
ComboBoxInputPrompt.prototype.getBodyHeight = function () {
  return this.panel.$element.outerHeight( true )*1.1;
};
// Set up with data passed at the time of opening (that can be different for each instance)
ComboBoxInputPrompt.prototype.getSetupProcess = function ( data ) {		
	data = data || {};
	return ComboBoxInputPrompt.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
		this.field.setLabel(data.label || '');
		this.field.setNotices( ( data.notice ) ? [data.notice] : [] );
		if ( data.options ) {
			this.input.setSuggestions( data.options );
		}/* else {
			this.input.getMenu().clearItems();
		}*/
		if ( data.confirmation && data.confirmation.isRequired && data.confirmation.message ) {
			this.confirmation = data.confirmation;
		} 
		this.updateSize();
	}, this );
};
// Processes to handle the actions.
ComboBoxInputPrompt.prototype.getActionProcess = function ( action ) {
	var self = this,
		value = self.input.getValue();
	if ( action === 'add' ) {
		var userConfirmationRequired = this.confirmation && this.confirmation.isRequired(value);
		if ( userConfirmationRequired ) {
			return new OO.ui.Process( function () {
				return OO.ui.confirm( self.confirmation.message(value), {title: 'Warning'} )
				.then(function(confirmed) {
					if (confirmed) {
						self.close({input: value});
					} else {
						self.input.focus();
					}
				});
			});
		} else {
			// Close dialog, passing through the input data
			return new OO.ui.Process( function () {
				self.close({input: value});
			});
		}
	}
	// Fallback to parent handler
	return ComboBoxInputPrompt.super.prototype.getActionProcess.call( this, action );
};
// Cleanup or other actions to perform whenever the dialog is closed
ComboBoxInputPrompt.prototype.getTeardownProcess = function ( data ) {
	return ComboBoxInputPrompt.super.prototype.getTeardownProcess.call( this, data )
	.first( function () {
		this.field.setLabel('');
		this.input.setValue('');
		if ( this.confirmation ) {
			delete this.confirmation;
		}
	}, this );
};
//

/* ---------- OverlayDialog --------------------------------------------------------------------- */
var OverlayDialog = function( config ) {
	OverlayDialog.super.call( this, config );
};
OO.inheritClass( OverlayDialog, OO.ui.MessageDialog );
OverlayDialog.static.name = 'overlayDialog';
OverlayDialog.prototype.clearMessageAndSetContent = function(contentHtml) {
	this.$element.find('label.oo-ui-messageDialog-message').after(
		$('<div>').addClass('oo-ui-overlayDialog-content').append(contentHtml)
	).empty();
};
OverlayDialog.prototype.getTeardownProcess = function ( data ) {
	this.$element.find('div.oo-ui-overlayDialog-content').remove();
	return OverlayDialog.super.prototype.getTeardownProcess.call( this, data );
};

/* ---------- Window manager -------------------------------------------------------------------- */
/* Factory.
   Makes it easer to use the window manager: Open a window by specifiying the symbolic name of the 
   class to use, and configuration options (if any). The factory will automatically crete and add
   windows to the window manager as needed. If there is an old window of the same symbolic name,
   the new version will automatically replace old one.
*/
var windowFactory = new OO.Factory();
windowFactory.register( ComboBoxInputPrompt );
windowFactory.register( OverlayDialog );

var windowManager = new OO.ui.WindowManager( { factory: windowFactory } );
windowManager.$element.addClass('rater-oouiWindowManager').appendTo('body');

/* ========== Dialog class ====================================================================== */
// Constructor
var Dialog = function(currentPage) {
	this.page = currentPage;
	
	// Make an new dialog/interface window	
	this.interfaceWindow = new Morebits.simpleWindow(
		Math.min(900, Math.floor(window.innerWidth*0.8)),
		Math.floor(window.innerHeight*0.9)
	);
	this.interfaceWindow.setTitle('Rater [v.'+config.script.version+']');
	this.interfaceWindow.addFooterLink('script documentation', 'WP:RATER');
	this.interfaceWindow.addFooterLink('feedback', 'WT:RATER');
	this.interfaceWindow.setContent(
		$('<div>')
		.attr('id', 'rater-dialog')
		.append(
			//$('<div>').attr('id', 'rater-dialog-header'),
			$('<div>').attr('id', 'rater-dialog-body')
		)
		.get(0)
	);
	$('a.ui-dialog-titlebar-close.ui-corner-all').remove();
	$('#rater-dialog').parent().css('background-color', '#f0f0f0');
	this.$footerButtons = $('#rater-dialog').parent().nextAll('.ui-dialog-buttonpane')
		.find('span.morebits-dialog-buttons');
	this.interfaceWindow.display();
};
// ---------- Dialog prototype ------------------------------------------------------------------ */
// Overlay dialog for previews and diffs
Dialog.prototype.showOverlayDialog = function(contentDeferred, heading, otherAction) {
	var self = this;

	var instance = windowManager.openWindow( 'overlayDialog', {
		title: heading,	
		message: 'Loading...',
		size: 'larger',
		actions: [
			{
				action: 'close',
				label: 'Close',
				flags: 'safe'
			},
			{
				action: 'save',
				label: 'Save changes',
				flags: ['primary', 'progressive']
			},
			{
				action: otherAction,
				label: 'Show ' + otherAction
			}
		]
	} );
	instance.opened.then( function() {
		contentDeferred.done(function(contentHtml){
			windowManager.getCurrentWindow().clearMessageAndSetContent(contentHtml);
			windowManager.getCurrentWindow().updateSize();
		})
		.fail(function(code, jqxhr) {
			windowManager.getCurrentWindow().clearMessageAndSetContent(
				heading + ' failed.',
				( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
			);
		});
	});
	instance.closed.then(function(data) {
		var footerButtonIndex = {
			'save': '0',
			'preview': '1',
			'changes': '2'
		};
		if ( !data || !data.action || !footerButtonIndex[data.action] ) {
			return;
        }
		self.$footerButtons.children().eq(footerButtonIndex[data.action]).click();
	});
};


// --- Basic manipulation: ---
// Append content to header
//Dialog.prototype.addToHeader = function($content) {
//	$('#rater-dialog-header').append($content);
//;
// Append content to body
Dialog.prototype.addToBody = function($content) {
	$('#rater-dialog-body').append($content);
};
// Add buttons to footer
Dialog.prototype.setFooterButtons = function($buttons, mode) {
	if ( mode === 'prepend' ) {
		this.$footerButtons.prepend($buttons);
	} else if ( mode === 'append' ) {
		this.$footerButtons.append($buttons);
	} else {
		this.$footerButtons.empty().append($buttons);
	}
};
// Clear dialog
Dialog.prototype.emptyContent = function() {
	$('#rater-dialog-body').empty();
};
// Display dialog
Dialog.prototype.display = function() {
	this.interfaceWindow.display();
};
// Reset height
Dialog.prototype.resetHeight = function() {
	this.interfaceWindow.setHeight(Math.floor(window.innerHeight*0.9));
};
// Close dialog
Dialog.prototype.close = function() {
	this.interfaceWindow.close();
};

// --- Make interface elements: ---
Dialog.icons = {
	'delete': 	{
		'source':	'/upwiki/wikipedia/commons/thumb/1/18/OOjs_UI_icon_close-ltr.svg/40px-OOjs_UI_icon_close-ltr.svg.png',
		'tooltip':	'Remove template'
	},
	'clear':	{
		'source':	'/upwiki/wikipedia/commons/thumb/5/54/OOjs_UI_icon_noWikiText-ltr.svg/40px-OOjs_UI_icon_noWikiText-ltr.svg.png',
		'tooltip':	'Clear parameters'
	},
	'bypass':	{
		'source':	'/upwiki/wikipedia/commons/thumb/5/5d/OOjs_UI_icon_newline-rtl.svg/40px-OOjs_UI_icon_newline-rtl.svg.png',
		'tooltip':	'Bypass redirect'
	},
	'subst':	{
		'source':	'/upwiki/wikipedia/commons/thumb/5/5d/OOjs_UI_icon_newline-rtl.svg/40px-OOjs_UI_icon_newline-rtl.svg.png',
		'tooltip':	'Bypass wrapper (subst:)'
	},
	'ores':		{
		'source':	'/upwiki/wikipedia/commons/thumb/5/51/Objective_Revision_Evaluation_Service_logo.svg/40px-Objective_Revision_Evaluation_Service_logo.svg.png',
		'tooltip':	'Machine-predicted quality from ORES'
	},
	'redirect':	{
		'source':	'/upwiki/wikipedia/en/thumb/8/89/Symbol_redirect_vote2.svg/40px-Symbol_redirect_vote2.svg.png',
		'tooltip':	'Page is a redirect'
	},
};

Dialog.makeIcon = function(iconName, clickable) {
	return $('<img>').attr({
		'src':		Dialog.icons[iconName].source,
		'title':	Dialog.icons[iconName].tooltip,
		'alt':		iconName,
		'width':	'20px',
		'height':	'20px'
	}).addClass('rater-dialog-'+iconName)
	.css( (clickable===false) ? {} : {'float':'left', 'cursor':'pointer', 'margin-right':'0.2em'});
};

Dialog.makeDropdown = function(values, selectedValue, param, data) {
	var $nullOption = $('<option>').attr('value', ' ').text(' ');
	
	var $dropdown = $('<select>')
	.addClass('rater-dialog-dropdown')
	.append( $nullOption )
	.append(
		values.map(function(val) {
			return $('<option>').attr('value', val).text(val);
		})
	);
	
	var $selected = ( selectedValue == null ) ? [] : $dropdown.children().filter(function(){
		return $(this).attr('value').toLowerCase() === selectedValue.toLowerCase();
	}).first();
	if ( $selected.length ) {
		$selected.attr('selected', 'selected');
	} else {
		$nullOption.attr('selected', 'selected');
	}
	
	if ( param == null ) {
		return $dropdown;
	}
	
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-dropdownContainer').append(
		( param ) ? Dialog.makeParamLabel(param, data && data.label, data && data.description) : '',
		$dropdown,
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);

};

Dialog.makeParamCheckbox = function(currentVal, param, data) {
	currentVal = currentVal || '';
	
	var valueIndex = $.inArray(currentVal.toLowerCase(), data.allowedValues );	
	// If existing value isn't one of the allowed values (or no value), switch to makeParamTextInput
	if ( valueIndex === -1 && currentVal !== '') {
		return Dialog.makeParamTextInput(currentVal, param, data);		
	}
	// valueIndex will now be 0 ('checked' value) or 1 ('not checked' value) or -1 (no value)
	
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-checkboxContainer').append(
		Dialog.makeParamLabel(param, data && data.label, data && data.description),
		$('<input>')
			.attr('type', 'checkbox')
			.prop('checked', !valueIndex)
			.data('values', {
				'true': data.allowedValues[0] || '',
				'false': data.allowedValues[1] || ''
			}),
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);
};

Dialog.makeParamTextInput = function(currentVal, param, data) {
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-textInputContainer').append(
		Dialog.makeParamLabel(param, data && data.label, data && data.description),
		$('<input>').attr('type', 'text').val(currentVal),
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);
};

Dialog.makeParamLabel = function(param, labeltext, description) {
	// Make label
	var $label = $('<label>');
	if ( labeltext ) {
		$label.append(
			$('<span>').addClass('rater-dialog-para-label')
				.text(labeltext)
				.attr('title', '|'+param+'= ' + (description || '')),
			$('<span>').text(param).hide()
		);
	} else {
		$label.append(
			$('<span>').addClass('rater-dialog-para-code').text(param)
		);
	}
	return $label;
};
Dialog.makeParamInput = function(currentVal, param, data) {
	if ( !data || !data.allowedValues ) {
		return Dialog.makeParamTextInput(currentVal, param, data);
	}
	if ( data.allowedValues.length <=2 ) {
		return Dialog.makeParamCheckbox(currentVal, param, data);
	}
	return Dialog.makeDropdown(data.allowedValues, currentVal, param, data);
};

Dialog.makeButton = function(options) {
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.keypress(function(e) {
		if ( e.which == 13 ) { $(this).click(); }
	})
	.children().css({'padding-top':'0.5em','padding-bottom':'0.4em'});
	return $button;
};
Dialog.makeFramelessButton = function(options) {
	options.framed = false;
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.css('padding','0').keypress(function(e) {
		if ( e.which == 13 ) { $(this).click(); }
	}).children().css({'padding-top':'0.3em','padding-bottom':'0.3em'})
		.children().css('font-weight','normal');
	return $button;
};

Dialog.prototype.makeRow = function(templateObject) {
	var self = this;
	var $row = $('<div>');
	
	// Whether class/importance parameters are used. Check if not in config.banners.withoutRatings
	//  array, since that one is significantly smaller than config.banners.withRatings
	var hasRatings = -1 === $.inArray(
		templateObject.getRedirectOrMainText(),
		config.banners.withoutRatings
	);

	var setParamHandlers = function() {
		var $span = $(this);
		var param = $span.children('label').children().last().text();
		var $input = $span.children('input');
		var $a = $span.children('a');
		
		if ( $input.attr('type') === 'text' ) {
			$input.blur(function() {
				if ( templateObject.parameters[param] !== $input.val().trim() ) {
					templateObject.setParamValue(param, $input.val().trim());
				}
			});
			$a.click(function() {
				templateObject.deleteParam(param);
				self.rebuildRow($row, templateObject);
			});
			
		} else if ( $input.attr('type') === 'checkbox' ) {
			$input.change(function() {
				templateObject.setParamValue(param, $input.data('values')[$input.prop('checked')]);
			});
			$a.click(function() {
				templateObject.deleteParam(param);
				self.rebuildRow($row, templateObject);
			});
			
		} else {
			var $dropdown = $span.children('select');
			$dropdown.change(function() {
				var $thisVal = $(this).val() || '';
				if (
					templateObject.parameters[param] &&
					templateObject.parameters[param].toLowerCase() === $thisVal.toLowerCase()
				) {
					return;
				}
				templateObject.setParamValue(param, $thisVal);
			});			
		}
	};
	

	var removeTemplate = Dialog.makeIcon('delete').click(function() {
		templateObject.remove = true;
		templateObject.isProjectBanner = false;
		$row.remove();
	});
	
	var clearTemplate = Dialog.makeIcon('clear').click(function() {
		$.each(templateObject.parameters, function(paraName) {
			templateObject.setParamValue(paraName, null);
		});
		self.rebuildRow($row, templateObject);
	});
	
	var templateName = $('<span>').addClass('rater-dialog-templateName').append(
		'{{',
		templateObject.getLinkedName(),
		'}}'
	);

	var bypassRedirect = '';
	if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
		if ( /^Subst\:/.test(templateObject.redirectsTo.getMainText()) ) {
			bypassRedirect = Dialog.makeIcon('subst').addClass('rater-dialog-bypass');
		} else {
			bypassRedirect =  Dialog.makeIcon('bypass');
		}
		bypassRedirect.click(function() {
			templateObject.bypassRedirect = true;
			templateName.empty().append(
				'{{',
				templateObject.getLinkedName(),
				'}}'
			);
			$(this).remove();
		});
	}
	
	var classParam = ( !hasRatings ) ? '' : Dialog.makeDropdown(
		templateObject.classes,
		templateObject.parameters.class,
		'class',
		{label:'Class', required:true}
	)
	.addClass('rater-dialog-row-class');
	
	var importanceParam = ( !hasRatings || templateObject.importances.length === 0 ) ? '' : Dialog.makeDropdown(
		templateObject.importances,
		templateObject.parameters.importance,
		'importance',
		{label:'Importance', required:true}
	)
	.addClass('rater-dialog-row-importance');

	var addParam = $('<span>').append(
		Dialog.makeFramelessButton({
			label:	'[add parameter]',
			icon:	'tableAddColumnBefore'
		})
	);
	addParam.click(function() {		
		var prompt = windowManager.openWindow( 'comboBoxInput', {
			label: 'Add parameter',
			options: templateObject.parameterSuggestions,
			notice: ( templateObject.notemplatedata !== true )
				? null
				: new OO.ui.HtmlSnippet(
					$('<span>').css({'color':'#555', 'font-size':'92%'}).append(
						'Using default parameter data, which may be inaccurate.',
						( templateObject.templatedataApiError )
							? 'TemplateData could not be retrieved due to an API error.'
							: $('<p>').append( 
								'This WikiProject banner has not been configured for use with this tool. See the ',
								extraJs.makeLink('User:Evad37/rater#TemplateData_quick_tutorial', 'TemplateData quick tutorial'),
								' or ask for help on ',
								extraJs.makeLink('User talk:Evad37/rater.js', ' the script\'s talk page'),
								'.'
						)
					)
				)
		} );
		prompt.opened.then( function() {
			windowManager.getCurrentWindow().input.focus();
		});
		prompt.closed.then( function ( data ) {
			//windowManager.clearWindows();
			if ( !data || !data.input ) {
				// No input data - ie cancelled
				return;
			}
			if ( templateObject.parameters[data.input] != null ) {
				alert('There is already a ' + data.input + ' parameter!');
				return;
			}
			templateObject.parameters[data.input] = templateObject.getDataForParam('autovalue', data.input) || '';
			self.rebuildRow($row, templateObject);
		} );
	});
	
	var otherParams = $('<div>').append(
		$.map(templateObject.parameters, function(val, param) {
			if (
				val === null ||
				( hasRatings && param === 'class' ) ||
				( hasRatings && param === 'importance' && templateObject.importances.length > 0 )
			) {
				return '';
			}
			var data = ( !templateObject.paramData ) ? null : {
				label: templateObject.getDataForParam('label', param),
				description: templateObject.getDataForParam('description', param),
				allowedValues: templateObject.getDataForParam('allowedValues', param),
				suggested: templateObject.getDataForParam('suggested', param),
				required: templateObject.getDataForParam('required', param),
				autovalue: templateObject.getDataForParam('autovalue', param)
			};
			return Dialog.makeParamInput(val, param, data);
		}),
		addParam
	);
	
	$().add(classParam)
	.add(importanceParam)
	.add(otherParams.children('span.rater-dialog-paraInput'))
	.each(setParamHandlers);
	
	return $row.addClass('rater-dialog-row').append(
		$('<div>').append(
			removeTemplate,
			clearTemplate,
			templateName,
			bypassRedirect,
			classParam,
			importanceParam
			//listasParam
		),
		otherParams
	);
};

Dialog.prototype.makeActionsRow = function() {
	var self = this;
	
	var addProject = Dialog.makeButton({
		label:	'Add WikiProject',
		icon:	'add',
		flags:	['progressive']
	})
	.click(function() {
		var prompt = windowManager.openWindow( 'comboBoxInput',	{
			label: 'Add Template:',
			options: config.bannerOptions,
			confirmation: {
				isRequired: function(value) {
					return !/^[Ww](?:P|iki[Pp]roject)/.test(value);
				},
				message: function(value) {
					return new OO.ui.HtmlSnippet(
						"<code>{{" + value + "}}</code> is not a recognised WikiProject banner.<br/>Do you want to continue?"
					);
				} 
			}
		} );
		prompt.opened.then( function() {
			windowManager.getCurrentWindow().input.focus();
		});
		prompt.closed.then( function ( data ) {
			if ( !data || !data.input ) { return; }
			
			var existingBanner = self.page.getBannerFromNameOrRedirect(data.input);
			
			if ( existingBanner && !existingBanner.remove ) {
				alert('There is already a {{' + existingBanner.getTransclusionName() + '}} banner!');
				return;
			}
			
			var templateObject;
			
			if ( existingBanner ) {
				existingBanner.remove = false;
				existingBanner.parameters = {};
				existingBanner.touched = {};
				templateObject = existingBanner;
			} else {
				templateObject = new Template('Template:'+data.input);
				templateObject.isProjectBanner = true;
				self.page.banners.push(templateObject);
			}
			
			var dabWarningDeferred = $.Deferred();
			var talkDoesNotExist = $('#ca-talk.new').length !== 0;
			var bannerIsWpDab    = templateObject.getTransclusionName() === 'WikiProject Disambiguation';
			var isOnlyBanner    = self.page.banners.filter(function(b){ return !b.remove; }).length === 1;
			if ( talkDoesNotExist && bannerIsWpDab && isOnlyBanner ) {
				OO.ui.confirm(
					"New talk pages shouldn't be created if they will only contain the {{WikiProject Disambiguation}} banner. Continue?",
					{title: 'Warning'}
				)
				.then(function(confirmed) {
					if ( confirmed ) {
						dabWarningDeferred.resolve('confirmed');
					} else {
						dabWarningDeferred.reject('cancelled');
						self.page.banners = self.page.banners.filter(function(b){
							return b.getTransclusionName() !== 'WikiProject Disambiguation';
						});
					}
				});
			} else {
				dabWarningDeferred.resolve('notApplicable');
			}
			
			// Retrieve extra data -- but always resolve because we can still work without it
			var redirectsToDeferred =  $.Deferred();			
			$.when(!!existingBanner || templateObject.setRedirectsTo())
			.always(function() { redirectsToDeferred.resolve(); });

			var classesAndImportancesDeferred = $.Deferred();
			$.when(!!existingBanner || templateObject.parseClassesAndImportances())
			.always(function() { classesAndImportancesDeferred.resolve(); });
			
			var templateDataDeferred = $.Deferred();
			$.when(!!existingBanner || templateObject.setParamDataAndSuggestions())
			.always(function() { templateDataDeferred.resolve(); });

			$.when(
				dabWarningDeferred,
				redirectsToDeferred,
				classesAndImportancesDeferred,
				templateDataDeferred
			)		
			.then( function() {
				// set required/suggested parameters to either the autovaule, or
				// an empty string (without touching, unless it is a required parameter)
				$.each(templateObject.paramData, function(paraName, paraData) {
					if ( paraData.required || paraData.suggested ) {

						templateObject.setParamValue(paraName, paraData.autovalue || '', !paraData.required);					
					}
				});
				var newRow = self.makeRow(templateObject).insertBefore('#rater-dialog-actions');
				self.autofillParams(newRow);
			} );
		} );
	});
	
	var removeAll = Dialog.makeButton({
		label:	'Remove all',
		icon:	'close',
		flags:	['destructive']
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			templateObject.remove = true;
		});
		$('div.rater-dialog-row').not('#rater-dialog-actions').remove();
	});
	
	var clearAll = Dialog.makeButton({
		label:	'Clear all',
		icon:	'noWikiText'
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			$.each(templateObject.parameters, function(paraName) {
				templateObject.setParamValue(paraName, null);
			});
		});
		self.refresh();
	});

	var bypassAllRedirects = '';
	if ( $('img.rater-dialog-bypass').length ) {
		bypassAllRedirects = Dialog.makeButton({
			label:	'Bypass redirects',
			icon:	'arrowNext'
		}).click(function() {
			$.each(self.page.banners, function(_index, templateObject) {
				if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
					templateObject.bypassRedirect = true;
				}
			});
			self.refresh();
		});
	}
	
	var classForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.classes).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-class > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Class'));
	
	var importanceForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.importances).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-importance > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Importance'));
	
	var setAll = Dialog.makeButton({
		label:	'Set all',
		icon:	'tag'
	});
	setAll.find('span.oo-ui-labelElement-label').append(
		classForAllDropdown,
		importanceForAllDropdown
	);
	
	var pageInfo = $('<div>');
	if ( self.page.oresScore ) {
		pageInfo.append(
			$('<div>').append(
				Dialog.makeIcon('ores', false),
				'&nbsp;',
				extraJs.makeLink('mw:ORES', 'ORES'),
				' Predicted class: ',
				$('<b>').text(self.page.oresScore)
			)
		);
	}
	if ( self.page.subjectRedirectsTo ) {
		pageInfo.append(
			$('<div>').append(
				Dialog.makeIcon('redirect', false),
				' Page redirects to: ',
				extraJs.makeLink(self.page.subjectRedirectsTo)
			)
		);
	}
	
	
	return $('<div>').attr('id', 'rater-dialog-actions').addClass('rater-dialog-row').append(
		pageInfo,
		$('<div>').append(
			addProject,
			removeAll,
			clearAll,
			bypassAllRedirects,
			setAll
		)
	);
};

Dialog.prototype.makeButtons = function() {
	if ( this.$footerButtons.children().length !== 0 ) {
		return;
	}
	
	var self = this;
	
	var cancel = Dialog.makeButton({
		label:	'Cancel',
		framed:	false,
		flags:	['destructive']
	}).click(function() { self.close(); });
	
	var save = Dialog.makeButton({
		label:	'Save changes',
		flags:	['primary', 'progressive'],
		accessKey: 's'
	}).click(function() { self.onSaveClick(); });
	
	var preview = Dialog.makeButton({
		label:	'Show preview',
		accessKey: 'p'
	}).click(function() {
		self.showOverlayDialog( self.page.makePreview(), 'Preview', 'changes' );
	});

	var showdiff = Dialog.makeButton({
		label:	'Show changes',
		accessKey: 'v'
	}).click(function() {
		self.showOverlayDialog( self.page.makeDiff(), 'Diff', 'preview' ); 
	});
	
	self.setFooterButtons([save, preview, showdiff, cancel]);
};
Dialog.prototype.onSaveClick = function() {
	var self = this;

	var close = Dialog.makeButton({
		label:	'Close'
	})
	.attr('id', 'rater-dialog-button-close')
	.click(function() { self.close(); });
	
	self.emptyContent();
	self.setFooterButtons(null);
	self.addToBody('Saving...');
	
	self.page.makeEdit()
	.done( function() {
		self.addToBody('Done!');
		self.setFooterButtons(close);
		$('#rater-dialog-button-close').find('a').focus();
		setTimeout(function() {
			$('#rater-dialog-button-close').click();
		}, 5000);
	})
	.fail( function(code, jqxhr) {
		self.addToBody('Failed. ' + extraJs.makeErrorMsg(code, jqxhr));
		self.setFooterButtons(close);
		$('#rater-dialog-button-close').find('a').focus();
	} );
};

Dialog.prototype.autofillParams = function($rowDiv) {
	var self = this;
	var $top = $rowDiv || $('#rater-dialog-body');
	
	// Autofill empty classes (if possible)
	var extrapolateClassFromExisting = function() {
		var classes = $('span.rater-dialog-row-class > select').map(function() {
			return $(this).val();
		}).get().sort();
		if ( -1 === $.inArray(' ', classes) ) {
			return false;
		}
		return classes.filter(function(c) {
			return c!== ' ';
		})[0];
	};
	var extrapolateClassFromOres = function() {
		if ( !self.page.oresScore ) {
			return null;
		}
		return ( self.page.oresScore === 'Stub' ) ? 'Stub' : 'Start';
	};
	var extrapolated = extrapolateClassFromExisting() || extrapolateClassFromOres();
	if ( extrapolated ) {
		$top.find('span.rater-dialog-row-class > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val(extrapolated).change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
	
	// Autofill empty importances to 'low' (articles only)
	if ( config.mw.wgNamespaceNumber <= 1 && !self.page.subjectRedirectsTo ) {
		$top.find('span.rater-dialog-row-importance > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val('Low').change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
	
	// Autofill listas parameter for WP:BIO
	var wpbioBanner = self.page.getBannerFromNameOrRedirect('WikiProject Biography');
	if ( wpbioBanner && !wpbioBanner.parameters.listas ) {
		var $wpbioRow = $('div.rater-dialog-row').has('span.rater-dialog-templateName:contains("'+
			wpbioBanner.getTransclusionName() + '")');
		var $listasInput = $wpbioRow.find('span.rater-dialog-paraInput').has('span:contains("listas")')
		.find('input');
		$listasInput.val(self.page.getListasAutofill()).blur();
		$listasInput.parent().addClass('rater-dialog-autofill')
		.on('keypress.first change.first', function() {
			$(this).removeClass('rater-dialog-autofill').off('keypress.first change.first');
		});
	}
};

Dialog.prototype.build = function(isInitialBuild) {
	var self = this;
	var isBuilt = $.Deferred();
	
	var parseBanners = '';
	if ( self.page.banners ) {
		parseBanners = self.page.banners.map(function(banner) {
			return banner.parseClassesAndImportances();
		});
	}
	
	$.when.apply(null, parseBanners).then(function() {
		if ( self.page.banners ) {
			self.addToBody(
				self.page.banners.map(function(banner) {
					if ( banner.remove ) {
						return '';
					}
					return self.makeRow(banner);
				})
			);
		}
		
		self.addToBody(self.makeActionsRow());
		
		if ( isInitialBuild && self.page.banners.length>0 ) {
			self.autofillParams();			
		}

		self.makeButtons();
		self.resetHeight();
		// Focus on Add WikiProject (first button within actions row)
		$("#rater-dialog-actions").find('span.oo-ui-buttonElement').first().find('a').focus();
		
		isBuilt.resolve();
	});
	return isBuilt;
};
	
Dialog.prototype.refresh = function() {
	this.emptyContent();
	this.build();
};

Dialog.prototype.rebuildRow = function(rowDiv, banner) {
	this.makeRow(banner).insertAfter(rowDiv);
	rowDiv.remove();
};

/* ========== Set up current page and dialog ==================================================== */
var setupRater = function(clickEvent) {
	if ( clickEvent ) {
		clickEvent.preventDefault();
	}
	
	var currentPage = Page.newFromText(config.mw.wgPageName);
	
	var progressBar = new OO.ui.ProgressBarWidget( {
		progress: 1
	} );
	var incrementProgress = function(amount, maximum) {
		var priorProgress = progressBar.getProgress();
		var incrementedProgress = Math.min(maximum || 100, priorProgress + amount);
		progressBar.setProgress(incrementedProgress);
	};
	var incrementProgressByInterval = function() {
		var incrementIntervalDelay = 100;
		var incrementIntervalAmount = 0.1;
		var incrementIntervalMaxval = 98;
		return window.setInterval(
			incrementProgress,
			incrementIntervalDelay,
			incrementIntervalAmount,
			incrementIntervalMaxval
		);
	};
	
	var dialog = new Dialog(currentPage);
	dialog.addToBody(
		$('<div>').attr('id', 'dialog-loading').append(
			progressBar.$element,
			$('<p>').attr('id', 'dialog-loading-0').css('font-weight', 'bold').text('Initialising:'),
			$('<p>').attr('id', 'dialog-loading-1').text('Loading talkpage wikitext...'),
			$('<p>').attr('id', 'dialog-loading-2').text('Parsing talkpage templates...'),
			$('<p>').attr('id', 'dialog-loading-3').text('Checking if page redirects...'),
			$('<p>').attr('id', 'dialog-loading-4').text('Retrieving quality prediction...').hide(),
			$('<p>').attr('id', 'dialog-loading-5').text('Building interface...')
		)
	);
	
	var showTaskDone = function(taskNumber) {
		$('#dialog-loading-'+taskNumber).append(' Done!');
		var isLastTask = ( taskNumber === 5 );
		if ( isLastTask ) {
			// Immediately show 100% completed
			incrementProgress(100);
			window.setTimeout(function() {
				$('#dialog-loading').hide();
			}, 100);
			return;
		} 
		// Show a smooth transition by using small steps over a short duration
		var totalIncrement = 20;
		var totalTime = 400;
		var totalSteps = 10;
		var incrementPerStep = totalIncrement / totalSteps;
		for ( var step=0; step < totalSteps; step++) {
			window.setTimeout(
				incrementProgress,
				totalTime * step / totalSteps,
				incrementPerStep
			);
		}
	};
	var showTaskFailed = function(taskNumber, code, jqxhr) {
		$('#dialog-loading-'+taskNumber).append(
			' Failed.',
			( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
		);
	};

	// Load and parse talk page
	var talkDeferred = currentPage.getTalkpageTopSection()
	.then(
		function() { showTaskDone(1); },
		function(code, jqxhr) { showTaskFailed(1, code, jqxhr); }
	)
	.then(function(){
		return currentPage.setTemplatesBanners()
		.then(
			function() { showTaskDone(2); },
			function(code, jqxhr) { showTaskFailed(2, code, jqxhr); }
		);
	});

	// Check if page is a redirect - but don't error out if request fails
	var redirDeferred = $.Deferred();
	currentPage.getSubjectRawWikitext()
	.always(function(rawPage) { 
		if ( /^\s*#REDIRECT/i.test(rawPage) ) {
			currentPage.subjectRedirectsTo = rawPage.slice(rawPage.indexOf('[[')+2, rawPage.indexOf(']]')) || true;
		}
		showTaskDone(3);
		redirDeferred.resolve();
	});

	// Retrieve rating from ORES
	var oresDeferred = ( config.mw.wgNamespaceNumber <= 1 ) ? $.Deferred() : '';
	if ( oresDeferred ) {
		$('#dialog-loading-4').show();
		redirDeferred.always(function() {
			if ( currentPage.subjectRedirectsTo ) {
				showTaskDone(4);
				oresDeferred.resolve();
				return;
			}
			currentPage.getOresScore()
			.then(
				function() {
					showTaskDone(4);
					oresDeferred.resolve();
				},
				function(code, jqxhr) {
					showTaskFailed(4, code, jqxhr);
					setTimeout(oresDeferred.resolve, 2000);
				}
			);
		});
	}

	// Build dialog
	$.when(oresDeferred, redirDeferred, talkDeferred)
	.then(function() {
		var incrementProgressInterval = incrementProgressByInterval();
		var dialogBuiltDeferred = dialog.build(true);
		dialogBuiltDeferred.always(function() {
			window.clearInterval(incrementProgressInterval);
		});
        return dialogBuiltDeferred;
    })
	.then(
		function(){
			showTaskDone(5);
            clearInvalidCacheItems();
		},
		function(code, jqxhr) { showTaskFailed(5, code, jqxhr); }
	);

};

// Add portlet link
mw.util.addPortletLink(
	'p-cactions',
	'#',
	'Rater',
	'ca-rater',
	'Rate quality and importance',
	'5'
);
$('#ca-rater').click(setupRater);

// Autostart if no wikiproject banners on talk page
(function autoStart() {
	if ( window.rater_autostartNamespaces == null || config.mw.wgIsMainPage ) {
		return;
	}
	
	var autostartNamespaces = ( $.isArray(window.rater_autostartNamespaces) ) ? window.rater_autostartNamespaces : [window.rater_autostartNamespaces];
	
	if ( -1 === autostartNamespaces.indexOf(config.mw.wgNamespaceNumber) ) {
		return;
	}
	
	if ( /(?:\?|&)(?:action|diff|oldid)=/.test(window.location.href) ) {
		return;
	}
	
	// Check if talk page exists
	if ( $('#ca-talk.new').length ) {
		setupRater();
		return;
	}
	
	var thisPage = Page.newFromText(config.mw.wgPageName);

	/* Check templates present on talk page. Fetches indirectly transcluded templates, so will find
	   Template:WPBannerMeta (and its subtemplates). But some banners such as MILHIST don't use that
	   meta template, so we also have to check for template titles containg 'WikiProject'
	*/
	API.get({
		action: 'query',
		format: 'json',
		prop: 'templates',
		titles: thisPage.getTalk(),
		tlnamespace: '10',
		tllimit: '500',
		indexpageids: 1
	})
	.done(function(result) {
		var id = result.query.pageids;
		var templates = result.query.pages[id].templates;
		
		if ( !templates ) {
			setupRater();
			return;
		}
		
		// Array.prototype.reduce is compatible with more browsers than Array.prototype.includes
		var hasWikiproject = templates.reduce(function(resultSoFar, currentTemplate) {
			return ( resultSoFar || /(WikiProject|WPBanner)/.test(currentTemplate.title) );
		}, false);
		
		if ( !hasWikiproject ) {
			setupRater();
			return;
		}
		
	})
	.fail(function(code, jqxhr) {
		// Silently ignore failures (just log to console)
		console.warn(
			'[Rater] Error while checking whether to autostart.' +
			( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
		);
	});

})();

/* ========== Export code for testing by /test.js =============================================== */
window.testRater = {
	'config': config,
	'API': API,/*
	'getListOfBanners': getListOfBanners,*/
	'Page': Page,
	'Template': Template,
	'SuggestionLookupTextInputWidget': SuggestionLookupTextInputWidget,
	'ComboBoxInputPrompt': ComboBoxInputPrompt,
	'OverlayDialog': OverlayDialog,
	'windowFactory': windowFactory,
	'windowManager': windowManager,
	'Dialog': Dialog,
	'setupRater': setupRater,
	'writeToCache': writeToCache/*,
	'autoStart': autoStart*/
};

/* ==========  End of file closure wrappers ===================================================== */
});
});
// </nowiki>