User:Trialpears/sandbox/XFDcloser.js

/* _____________________________________________________________________________ * |                                                                             | * |                    === WARNING: GLOBAL GADGET FILE ===                      | * |                 Changes to this page affect many users. | * | Please discuss changes on the talk page or on WT:Gadget before editing. | * |_____________________________________________________________________________| * * Imported from version 925484053 as of 14 November 2019 from User:Evad37/XFDcloser/v3.js * XFDcloser: close deletion discussions at XfD venues, including actions to implement closes; see WP:XFDC * Author: Evad37 * Licencing and attribution: WP:XFDC */ /* jshint esversion: 5, laxbreak: true, undef: true, eqnull: true, maxerr: 3000 */ /* globals console, document, window, $, mw, OO, extraJs, Morebits */ /* */ var XFDC_VERSION = "3.16.2";

// Record script start time ASAP var XFDC_START_TIME = new Date;

$.ready.then(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: ' (XFDcloser)', version: XFDC_VERSION }; // MediaWiki configuration values config.mw = mw.config.get( [	'wgPageName',	'wgUserGroups',	'wgFormattedNamespaces' ] );

/* - Quick checks that script should be running - */ if (	!config.mw.wgUserGroups.includes('extendedconfirmed') &&	!config.mw.wgUserGroups.includes('sysop') ) { // User is not extendedconfirmed or sysop return; }

/* - Additional configuration --- */ config.isMobileSite = window.location.host.includes(".m.") || window.location.search.includes("useformat=mobile"); // Set custom version of namespaces with description for namespace 0 config.mw.namespaces = $.extend({}, config.mw.wgFormattedNamespaces, {0: 'article'}); // Month names - no longer provided by mw.config, see phab:T219340 config.wgMonthNames = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; // 0-indexed month names (remove "" from start of array) config.monthNames = config.wgMonthNames.slice(1); // Set sysop status if ( config.mw.wgUserGroups.includes('sysop') ) { config.user = { 'isSysop': true, 'sig': '~' }; } else { config.user = { 'isSysop': false, 'sig': ' (non-admin closure) ~' }; } // Start time, for detecting edit conflicts config.startTime = XFDC_START_TIME; // Variables for tracking across multiple discussions config.track = { // Track Afd logpage edits using deferred objects, to know when it is safe to read the wikitext 'afdLogEdit': [$.Deferred.resolve], // Track how many closes/relists have been started and completed 'started': 0, 'finished': 0, 'discussions': [] };

//Warn if unloading before closes/relists are completed $(window).on('beforeunload', function(e) {	if ( config.track.started > config.track.finished ) {		e.returnValue = ;		return ;			} });

// ========== Convinenance functions ============================================================ */ /** * Un-escapes some HTML tags (, , , , , , , and ); * turns wikilinks into real links. Ignores anything within ... tags -- unless * wrapped with (e.g. `` is replaced with a real tag). * Input will first be escaped using mw.html.escape unless specified * @param {String} text * @param {Object} config Configuration options * @config {Boolean} noEscape - do not escape the input first * @returns {String} unescaped text */ var safeUnescape = function(text, config) { return ( config && config.noEscape && text || mw.html.escape(text) ) // Step 1: unescape tags .replace( 		/&lt;(\/?pre\s?\/?)&gt;/g,		'<$1>'	) // Step 2: replace piped wikilinks with real links (unless inside tags) .replace( 		/\[\[([^\|\]]*?)\|([^\|\]]*?)\]\](?![^<]*?<\/pre>)/g,		'$2'	) // Step 3: replace other wikilinks with real links (unless inside tags) .replace( 		/\[\[([^\|\]]+?)]\](?![^<]*?<\/pre>)/g,		'$1'	) // Step 4: unescape other tags (unless inside tags) .replace(		/&lt;(\/?(?:br|p|ul|li|hr|strong|em)\s?\/?)&gt;(?![^<]*?<\/pre>)/g,		'<$1>'	) // Step 5: unescape tags warpped in 	.replace(		/&lt;(\/?(?:br/g,		'<$1>'	); };

var formatErrorMessage = function(details) { var type = details && details.type || 'Script'; var error = details && details.error || 'Unknown error'; var message = ( details.message ) ? ' - ' + safeUnescape(details.message) : '';

return type + ' error: ' + error + message; };

// TODO: how to pass in a message.... or maybe this should just return {type:..., error:...} objects? var formatApiError = function(code, jqxhr, message) { if ( code === 'http') { var httpError = ( jqxhr.textStatus === 'error') ? jqxhr.xhr.status : jqxhr.textStatus; return formatErrorMessage({			type: 'HTTP',			error: httpError,			message: message		}); }

if ( code === 'okay-but-empty' ) { return formatErrorMessage({			type: 'Server',			error: 'Got an empty response from the server'		}); }

return formatErrorMessage({		type: 'API',		error: code,		message: message	}); };

/* ========== XfdVenue class ==================================================================== Each instance represents an XfD venue, with properties/function specific to that venue -- */ // Constructor var XfdVenue = function(type, settings) { this.type = type; for ( var key in settings ) { this[key] = settings[key]; } }; // -- XfdVenue prototype --- */ XfdVenue.prototype.hasNomTemplate = function(wikitext) { var pattern = new RegExp(this.regex.nomTemplate); return pattern.test(wikitext); }; XfdVenue.prototype.removeNomTemplate = function(wikitext) { var pattern = new RegExp(this.regex.nomTemplate); var matches = wikitext.match(pattern); if ( !matches ) { return wikitext; }	if ( matches.length > 1 ) { throw new Error('Multiple nomination templates on page'); }	return wikitext.replace(pattern, ''); }; XfdVenue.prototype.updateNomTemplateAfterRelist = function(wikitext, today, sectionHeader) { var matches = wikitext.match(this.regex.relistPattern); if ( !matches ) { return wikitext; }	if ( matches.length > 1 ) { throw new Error('Multiple nomination templates on page'); }	return wikitext.replace(		this.regex.relistPattern,		this.wikitext.relistReplace			.replace("__TODAY__", today)			.replace("__SECTION_HEADER__", sectionHeader)	); }; // -- Venue-specific instances --- */ // MFD XfdVenue.Mfd = function { var venue = new XfdVenue('mfd', {		path:		 'Wikipedia:Miscellany for deletion',		subpagePath: 'Wikipedia:Miscellany for deletion/',		ns_number:	 null,		html: {			head:			'h4',			list:			'dl',			listitem:		'dd'		},		wikitext: {			closeTop:		" __RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			oldXfd:			""+				"\n",			mergeFrom:		"\n",			mergeTo:		"\n",			alreadyClosed:	""		},		regex: {			nomTemplate:	/(?: \s*)?(?:]*}}| \[\[Category:Miscellaneous pages for deletion\|?.*\]\]\s*)(?:\s*<\/noinclude>)?/gi		}	}); return venue; };

// CFD XfdVenue.Cfd = function { var venue = new XfdVenue('cfd', {		path:		 'Wikipedia:Categories for discussion/Log/',		ns_number:	 [14],		html: {			head:			'h4',			list:			'ul',			listitem:		'li',			nthSpan:		'2'		},		wikitext: {			closeTop:		" __RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			oldXfd:			"\n",			alreadyClosed:	"",			relistReplace:	" full|day=__DAY__|month=__MONTH__|year=__YEAR__",					},		regex: {			nomTemplate:	/(?:.|\n)+\n*/gi,			relistPattern:	/ full\|day\=\d\d?\|month=\w+\|year\=\d{4}/gi		}	}); // Override prototype venue.updateNomTemplateAfterRelist = function(wikitext, today, _sectionHeader) { var matches = wikitext.match(venue.regex.relistPattern); if ( !matches ) { return wikitext; }		if ( matches.length > 1 ) { throw new Error('Multiple nomination templates on page'); }		var todayParts = today.split(' '); return wikitext.replace(			venue.regex.relistPattern,			venue.wikitext.relistReplace				.replace("__DAY__", todayParts[2])				.replace("__MONTH__", todayParts[1])				.replace("__YEAR__", todayParts[0])		) .replace( // is a bit different to the other CFD nomination template			/\'\'\'\[\[Wikipedia\:Categories for discussion\/Log\/\d{4} \w+ \d{1,2}\#/,			"'''[[Wikipedia:Categories for discussion/Log/" + today + "#"		);	};	return venue; };

// FFD XfdVenue.Ffd = function { var venue = new XfdVenue('ffd', {		path:		 'Wikipedia:Files for discussion/',		ns_number:	 [6],		ns_unlink:  ['0', '10', '100', '118'], // main, Template, Portal, Draft		html: {			head:			'h4',			list:			'dl',			listitem:		'dd',			nthSpan:		'1'		},		wikitext: {			closeTop:		"__RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			oldXfd:			"\n",			pagelinks:		"__PAGE__\n",			relistReplace:	"{{ffd|log=__TODAY__",			alreadyClosed:	""				},		regex: {			nomTemplate:	/]*}}/gi,			relistPattern:	/]*/gi		}	}); return venue; };

// TFD XfdVenue.Tfd = function { var venue = new XfdVenue('tfd', {		path:		 'Wikipedia:Templates for discussion/Log/',		subpagePath: 'Wikipedia:Templates for discussion/',		ns_number:	 [10, 828],		html: {			head:			'h4',			list:			'ul',			listitem:		'li',			nthSpan:		'1'		},		wikitext: {			closeTop:		"__RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			oldXfd:			"\n",			pagelinks:		"* __PAGE__\n",			relistReplace:	"Wikipedia:Templates for discussion/Log/__TODAY__#",			alreadyClosed:	""		},		regex: {			nomTemplate:	/( [\n\s]*)?[^}}]*)*?}}([\n\s]*<\/noinclude>)?(\n)?/gi, relistPattern:	/Wikipedia:Templates(_|\s){1}for(_|\s){1}discussion\/Log\/\d{4}(_|\s){1}\w*(_|\s){1}\d{1,2}#(?=[^}]*}{2})/gi },		holdingCellSectionNumber: { "review":			3, "merge-infobox":	5, "merge-navigation":	6, "merge-link":		7, "merge-other":		8, "merge-meta":		9, "convert":			10, "substitute":		11, "orphan":			12, "ready":			13	// (ready for deletion) }	});	// Override prototype	venue.removeNomTemplate = function(wikitext) {		var pattern = new RegExp(venue.regex.nomTemplate);		var matches = wikitext.match(pattern);		if ( !matches ) {			return wikitext;		}		if ( matches.length > 1 ) {			throw new Error('Multiple nomination templates on page');		}		var tags = pattern.exec(wikitext);		if ( !tags ) {			return wikitext;		}		var logical_xor = function(first, second) {			return (first ? true : false) !== (second ? true : false);		};		var unbalancedNoincludeTags = logical_xor(tags[1], tags[2]);		var replacement = ( unbalancedNoincludeTags ) ? "$1$2" : "";		return wikitext.replace(pattern, replacement);	};	venue.updateNomTemplateAfterRelist = function(wikitext, today, sectionHeader) {		var matches = wikitext.match(venue.regex.relistPattern);		if ( !matches ) {			return wikitext;		}		if ( matches.length > 1 ) {			throw new Error('Multiple nomination templates on page');		}		return wikitext.replace( venue.regex.relistPattern, venue.wikitext.relistReplace .replace("__TODAY__", today) .replace("__SECTION_HEADER__", sectionHeader) );	};	return venue; };

// RFD XfdVenue.Rfd = function { var venue = new XfdVenue('rfd', {		type:		 'rfd',		path:		 'Wikipedia:Redirects for discussion/Log/',		ns_number:	 null,		html: {			head:			'h4',			list:			'ul',			listitem:		'li'		},		wikitext: {			closeTop:		"__RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			oldXfd:			"\n",			alreadyClosed:	"",			relistReplace:	"#invoke:RfD||2=__SECTION_HEADER__|"		},		regex: {			nomTemplate:		/(^\s*|\[\[Category:Temporary maintenance holdings\]\]\n?)/g,			fullNomTemplate:	/(^\s*|\[\[Category:Temporary maintenance holdings\]\]\n?)/g,			relistPattern: 	/#invoke:RfD\|\|\|/gi		},	}); // Override prototype venue.removeNomTemplate = function(wikitext) { var pattern = new RegExp(venue.regex.nomTemplate); return wikitext.replace(pattern, ''); };	return venue; };

// AFD XfdVenue.Afd = function(transcludedOnly) { var venue = new XfdVenue('afd', {		type:		 'afd',		path:		 'Wikipedia:Articles for deletion/Log/',		subpagePath: 'Wikipedia:Articles for deletion/',		ns_number:	 [0], // main		ns_logpages: 4, // Wikipedia		ns_unlink:  ['0', '10', '100', '118'], // main, Template, Portal, Draft		html: {			head:			'h3',			list:			'dl',			listitem:		'dd',			nthSpan:		'2'		},		wikitext: {			closeTop:		"__RESULT____TO_TARGET____RATIONALE__ __SIG__",			closeBottom:	"",			mergeFrom:		"\n",			mergeTo:		"\n",			alreadyClosed:	""				},		regex: {			nomTemplate:	/(?:{{[Aa](?:rticle for deletion\/dated|fDM|fd\/dated)|)?\s*/g		},		transcludedOnly:	transcludedOnly	}); return venue; };

XfdVenue.newFromPageName = function(pageName) { // Create xfd venue object for this page var isAfd = /(Articles_for_deletion|User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(pageName); var afdTranscludedOnly = /(User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(pageName); if ( pageName.includes('Wikipedia:Miscellany_for_deletion') ) { return XfdVenue.Mfd; } else if ( pageName.includes('Categories_for_discussion/') ) { return XfdVenue.Cfd; } else if ( pageName.includes('Files_for_discussion') ) { return XfdVenue.Ffd; } else if ( pageName.includes('Templates_for_discussion') ) { return XfdVenue.Tfd; } else if ( pageName.includes('Redirects_for_discussion') ) { return XfdVenue.Rfd; } else if ( isAfd ) { return XfdVenue.Afd(afdTranscludedOnly); } else { throw new Error('"' + pageName + '" is not an XFD page'); } };

config.xfd = XfdVenue.newFromPageName(config.mw.wgPageName);

if (window.XFDC_SANDBOX) config = window.XFDC_MAKE_SANDBOX_CONFIG(config); // Adjust some settings if running in sandbox mode

/* ========== API =============================================================================== */ var API = new mw.Api( {	ajax: {		headers: { 			'Api-User-Agent': 'XFDcloser/' + config.script.version + 				' ( https://en.wikipedia.org/wiki/WP:XFDC )'		}	} } );

// Helper functions for processing results var arrayFromResponsePages = function(response) { return $.map(response.query.pages, function(page) { return page; }); };

var pageFromResponse = function(response) { return arrayFromResponsePages(response)[0]; };

API.editWithRetry = function(titles, getParams, transform, onEachSuccess, onEachFail) { var processPage = function(page, id, starttime) { var basetimestamp = page.revisions && page.revisions[0].timestamp; var simplifiedPage = { pageid: page.pageid, missing: page.missing === "", redirect: page.redirect === "", categories: page.categories, ns: page.ns, title: page.title, content: page.revisions && page.revisions[0].slots.main["*"] };		return $.when( transform(simplifiedPage) ) .then(function(editParams) {			var query = $.extend( { action: 'edit', title: page.title, // Protect against errors and conflicts assert: 'user', basetimestamp: basetimestamp, starttimestamp: starttime }, editParams );			var doEdit = function(isRetry) {				return API.postWithToken('csrf', query)				.then( function(data) { if (onEachSuccess) { onEachSuccess(data); }						return data.edit; },					function(code, error) { if (code === 'http' && !isRetry) { return doEdit(true); } else if ( code === 'editconflict' ) { return doGetQuery(page.title); }						if (onEachFail) { onEachFail(code, error, page.title); }						return $.Deferred.reject(code, error, page.title); }				);			};			return doEdit;		}, function(code, error) {			if (onEachFail) {				onEachFail(code, error, page.title);			}			return $.Deferred.reject(code, error, page.title);					}); };	var doGetQuery = function(titles, isRetry) { return API.get(			$.extend( {					"action": "query", "format": "json", "curtimestamp": 1, "titles": titles, "prop": "revisions|info", "rvprop": "content|timestamp", "rvslots": "main" },				getParams )		).then(function(response) {			var starttime = response.curtimestamp;			// Get an array of promises of edits (which may either be resolved or rejected)			var pages = $.map(response.query.pages, function(page, id) { return processPage(page, id, starttime); });			// If only for one title, return that promise (which may either be resolved or rejected)			if (!$.isArray(titles)) {				return pages[0];			}

// Otherwise, convert the array of promises into a single promise, resolved if all were // resolved, or rejected with an array of errors of all that failed. return $.when.apply(				null,				// Force promises to resolve as an object with a `success` key, so that				// $.when will wait for all of them to be resolved or rejected				pages.map(function(page) { return page.then(						function(success) {							return {success: true};						},						function(code, error, title) {							return {								success: false, 								code: code,								error: error,								title: title							};						}					); // end page.then }) // end page.map			) // end $.when .then(function {				var args = Array.prototype.slice.call(arguments);				var errors = args.filter(function(arg) { return !arg.success; });				if (errors.length > 0) {					return $.Deferred.reject("write", errors);				}				return true;			}); }, function(code, error) { if (!isRetry) { return doGetQuery(titles, true); }			return $.Deferred.reject("read", code, error); });	};	return doGetQuery(titles); };

var dmyDateString = function(date) { return date.getUTCDate.toString + ' ' + config.monthNames[date.getUTCMonth] + ' ' + date.getUTCFullYear.toString; };

/** * Generates a JS Date object from the text of a timestamp * @param {String} sigTimestamp in format "`hh`:`mm`, `d` `Month` `yyyy` (UTC)", e.g. "09:42, 11 January 2019 (UTC)" * @returns {Date|NaN} Date object, or NaN if sigTimestamp could not be parsed */ var dateFromSigTimestamp = function(sigTimestamp) { var pattern = /(\d\d\:\d\d), (\d{1,2}) (\w+) (\d\d\d\d) \(UTC\)/; var parts = pattern.exec(sigTimestamp); if ( parts === null ) { return NaN; }	var year = parts[4]; var monthIndex = config.wgMonthNames.indexOf(parts[3]); if ( monthIndex === -1 ) { return NaN; }	var month = ( monthIndex < 10 ) ? '0' + monthIndex : monthIndex; var day = ( parts[2].length === 1 ) ? '0' + parts[2] : parts[2]; var time = 'T' + parts[1] + 'Z'; var iso8601DateString = year + '-' + month + '-' + day + time; return Date.parse(iso8601DateString) && new Date(iso8601DateString); };

/* ========== Additional functions for working with mw.Title objects ============================ */ var hasCorrectNamespace = function(mwTitleObject) { return (		config.xfd.ns_number === null ||		config.xfd.ns_number.includes(mwTitleObject.getNamespaceId)	); }; var setExistence = function(mwTitleObject, exists) { mw.Title.exist.set(mwTitleObject.toString, exists); };

/* ========== Multibutton confirm dialog ======================================================== */

/** multiButtonConfirm * @param {Object} config * @config {String} title Title for the dialogue * @config {String} message Message for the dialogue. HTML tags (except for, , , * , , and tags) are escaped; wikilinks are turned into real links. * @config {Array} actions Optional. Array of configuration objects for OO.ui.ActionWidget * . * If not specified, the default actions are 'accept' (with label 'OK') and 'reject' (with *  label 'Cancel'). * @config {String} size Symbolic name of the dialog size: small, medium, large, larger or full. * @return {Promise} action taken by user */ var multiButtonConfirm = function(config) { var dialogClosed = $.Deferred; // Wrap message in a HtmlSnippet to prevent escaping var htmlSnippetMessage = new OO.ui.HtmlSnippet(		safeUnescape(config.message)	);

var windowManager = new OO.ui.WindowManager; var messageDialog = new OO.ui.MessageDialog; $('body').append( windowManager.$element ); windowManager.addWindows( [ messageDialog ] ); windowManager.openWindow( messageDialog, {		'title': config.title,		'message': htmlSnippetMessage,		'actions': config.actions,		'size': config.size	} ); windowManager.on('closing', function(_win, promise) {		promise.then(function(data) { dialogClosed.resolve(data && data.action); windowManager.destroy; });	});

return dialogClosed.promise; };

/* ========== Discusssion class ================================================================= Each instance represents an XfD discussion. -- */ /** Constructor * @param {Object} discussionConfig configuration object: *  @config {String} id - A unique ID for this discussion (used in various elements' IDs) *  @config {String} nomPage - Page where the XFD discussion is taking place; either a dated or *           name-based subpage *  @config {String} sectionHeader - text of the section heading for the XFD discussion *  @config {String} sectionNumber - edit section number for the XFD discussion *  @config {mw.Title[]} pages - pages nominated in the XFD discussion; omit if in basic mode *  @config {String} firstDate - first timestamp date, for RFD discussions *  @config {Boolean} isOld - `true` if discussion has been listed (or relisted) for more than 7 days *  @config {Boolean} isRelisted - `true` if discussion has been relisted * */ var Discussion = function(discussionConfig) { var defaultConfig = { pages: [], deferred: {} // For later tracking of jQuery Deferred objects };	$.extend(this, defaultConfig, discussionConfig); }; // Construct from headline span element Discussion.newFromHeadlineSpan = function (headingIndex, context) { var $headlineSpan = $(context); var $heading = $headlineSpan.parent; var $statusContainer = config.isMobileSite ? $heading.next : $heading; // Fix for "Auto-number headings" preference $('.mw-headline-number', context).prependTo($heading);

// Get section header var sectionHeader = $headlineSpan.text.trim;

// Check if already closed. Closed AfDs and MfDs have the box above the heading. if ( /(afd|mfd)/.test(config.xfd.type) && $heading.parent.attr('class') && $heading.parent.attr('class').includes('xfd-closed') ) { // Skip return; } else if ( !/(afd|mfd)/.test(config.xfd.type) && $heading.next.attr('class') ) { // Only for closed discussion will the next element after the heading have any class set // Skip, add class to enable hiding of closed discussions $heading.addClass('xfd-closed'); return; }

var sectionlink = $heading.find('.mw-editsection a') .not('.mw-editsection-visualeditor, .autoCloserButton').attr('href'); if (!sectionlink) { // Try to find a section link generated by Module:XfD_old. sectionlink = $heading.next.find(".xfdOldSectionEdit > a").attr("href"); if (!sectionlink) { // XFDcloser can't work without knowing the nompage and section number, so skip this section. return; }		// Add a "T-" so the next check will see this as a transcluded section sectionlink = sectionlink.replace("section=", "section=T-"); }	var editsection = sectionlink.split('section=')[1].split('&')[0]; var nompage = ''; if ( /T/.test(editsection) ) { // Section is transcluded from another page nompage = mw.Title.newFromText(			decodeURIComponent(sectionlink.split("title=")[1].split("&")[0])		).getPrefixedText; if ( -1 !== $.inArray(nompage, [ 'Wikipedia:Redirects for discussion/Header', 'Wikipedia:Redirect/Deletion reasons', 'Wikipedia:Templates for discussion/Holding cell', 'Wikipedia:Categories for discussion/Speedy' ])		) {			// ignore headings transcuded from these pages return; }		// remove "T-" from section number editsection = editsection.substr(2); } else { // Section is on current page, not transcluded if ( config.xfd.transcludedOnly ) { return; }		nompage = mw.Title.newFromText( config.mw.wgPageName ).getPrefixedText; }

var pages=[]; var firstDate;

if ( config.xfd.type === 'cfd' ) { //CFDs: Nominates pages are the first link of an  item in a  list, within a  list pages = $heading .nextUntil(config.xfd.html.head + ', div.xfd-closed') .find('dd > ul > li') .has('b:first-child:contains("Propose ")') .find('a:first-of-type') .not('.external') .map(function { return mw.Title.newFromText($(this).text); }) .get; if ( pages.length === 0 ) { // Sometimes nominated pages are instead just in a  list, e.g.			// Wikipedia:Categories_for_discussion/Log/2019_February_5#Foo_in_fiction pages = $heading .next('ul') .find('li') .find('a:first-of-type') .not('.external') .map(function { return mw.Title.newFromText($(this).text); }) .get; }	} else if ( config.xfd.type === 'rfd' || config.xfd.type === 'mfd' ) { // For MFD, closed discussion are within a collapsed table $('table.collapsible').has('div.xfd-closed').addClass('xfd-closed'); // MFD & RFD have nominated page links prior to span with classes plainlinks, lx		pages = $heading .nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed') .find(config.xfd.html.listitem) .has('span.plainlinks.lx') .children('span') .filter(':first-child') .children('a, span.plainlinks:not(.lx)') .filter(':first-child') .map(function { return mw.Title.newFromText($(this).text); }) .get; if ( config.xfd.type === 'rfd' ) { var discussionNodes = $heading .nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed') .clone; // Fix for "Comments in Local Time" gadget discussionNodes.find('span.localcomments').each(function{				var utcTime = $(this).attr('title');				$(this).text(utcTime);			}); var discussionText = discussionNodes.text; // Ignore relisted discussions, and non-boxed closed discussions if (				discussionText.includes('Relisted, see Wikipedia:Redirects for discussion') ||				discussionText.includes('Closed discussion, see full discussion')			) { return; }			// Find first timestamp date var firstDatePatt = /(?:\d\d:\d\d, )(\d{1,2} \w+ \d{4})(?: \(UTC\))/; var firstDateMatch = firstDatePatt.exec(discussionText); firstDate = firstDateMatch && firstDateMatch[1]; }	} else { // AFD, FFD, TFD: nominated page links inside span with classes plainlinks, nourlexpansion pages = $heading .nextUntil(config.xfd.html.head + ', div.xfd-closed') .find(config.xfd.html.listitem + ' > span.plainlinks.nourlexpansion') .filter(':nth-of-type(' + config.xfd.html.nthSpan + ')') .children('a') .filter(':first-child') .map(function { return mw.Title.newFromText($(this).text); }) .get; }	// Sanity check - check if any pages are null if ( !pages || pages.length === 0 || pages.some(function(p) { return !p; }) ) { //Still offer a "basic" close using the section header pages = false; }

// Check discussion age (since last relist, if applicable) // TODO: reduce redundancy with finding RfDs' first date var isOld; var $discussionNodes = $heading .nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed') .clone .find('span.localcomments') .each(function{				var utcTime = $(this).attr('title');				$(this).text(utcTime);			}) .end; var lastRelist = $(' ').append($discussionNodes).find('.xfd_relist').last.text; if ( lastRelist ) { $statusContainer.addClass('xfdc-relisted'); }	var notTranscludedCorrectlyPatt = /(?:Automated|Procedural) (?:comment|Note).*transcluded.*/i; var notTranscludedCorrectlyMatch = $discussionNodes.text.match(notTranscludedCorrectlyPatt); var notTranscludedCorrectlyComment = notTranscludedCorrectlyMatch && notTranscludedCorrectlyMatch[0];

var timestampPatt = /\d\d:\d\d, \d{1,2} \w+ \d{4} \(UTC\)/; var listingTimestampMatch = lastRelist.match(timestampPatt) || notTranscludedCorrectlyComment && notTranscludedCorrectlyComment.match(timestampPatt) || $discussionNodes.text.match(timestampPatt); var listingTimestampDate = listingTimestampMatch && dateFromSigTimestamp(listingTimestampMatch[0]); if ( !listingTimestampDate ) { $statusContainer.addClass('xfdc-unknownAge'); } else { var millisecondsSinceListing = new Date - listingTimestampDate; var discussionRuntimeDays = 7; var discussionRuntimeMilliseconds = discussionRuntimeDays * 24 * 60 * 60 * 1000; isOld = millisecondsSinceListing > discussionRuntimeMilliseconds; $statusContainer.addClass((isOld ? 'xfdc-old' : 'xfdc-notOld')); }

// Create status span and notices div with unique id based on headingIndex var uniqueID = 'XFDC' + headingIndex; var $statusSpan = $(' ') .attr({'id':uniqueID, 'class':'xfdc-status'}) .text('[XFDcloser loading...]'); var $noticesDiv = $(' ').attr({'id':uniqueID+'-notices', 'class':'xfdc-notices'}); if (config.isMobileSite) { $heading.next.prepend( $statusSpan, $noticesDiv ); } else { $headlineSpan.after( $statusSpan ); $heading.after($noticesDiv); }	// Create discussion object return new Discussion({		'id': uniqueID,		'nomPage': nompage,		'sectionHeader': sectionHeader,		'sectionNumber': editsection,		'pages': pages || [],		'firstDate': firstDate || null,		'isOld': !!isOld,		'isRelisted': !!lastRelist	}); }; // -- Discusssion prototype - */ // Get status element (jQuery object) Discussion.prototype.get$status = function { return $('#'+this.id); }; // Set status Discussion.prototype.setStatus = function($status) { this.get$status.empty.append($status); }; /** * Open dialog * * @param {Boolean} relisting open in relisting mode * @returns {Boolean} Dialog was opened */ Discussion.prototype.openDialog = function(relisting) { if (document.getElementById('xfdc-dialog')) { // Another dialog is already open return false; }	this.dialog = new Dialog(this, !!relisting); this.dialog.setup; return true; }; // Mark as finished Discussion.prototype.setFinished = function(aborted) { var self = this; var msg; if ( aborted != null ) { msg = [ $(' ').text( ( self.dialog && self.dialog.relisting ) ? 'Aborted relist' : 'Aborted close' ), ( aborted === '' ) ? '' : ': ' + aborted ];			} else if ( self.dialog && self.dialog.relisting ) { msg = [ 'Discussion ', $(' ').text('relisted'), ' (reload page to see the actual relist)' ];	} else { msg = [ 'Closed as ', $(' ').text(self.taskManager.inputData.getResult), ' (reload page to see the actual close)' ];	}	self.setStatus(msg); self.get$status.prev.css('text-decoration', 'line-through'); }; // Get notices element (jQuery object) Discussion.prototype.get$notices = function { return $('#'+this.id+'-notices'); }; // Set notices element Discussion.prototype.setNotices = function($content) { this.get$notices.empty.append($content); }; // Get an array of page titles Discussion.prototype.getPageTitles = function(pagearray, options) { var titles = (pagearray || this.pages).map(function(p) { 		return p.getPrefixedText;	}); if ( options && options.moduledocs ) { return titles.map(function(t) {			var isModule = ( t.indexOf('Module:') === 0 );			return ( isModule ) ? t + '/doc' : t;		}); }	return titles; }; // Get an array of page' talkpage titles (excluding pages which are themselves talkpages) Discussion.prototype.getTalkTitles = function(pagearray) { return (pagearray || this.pages).map(function(p) { 		return p.getTalkPage.getPrefixedText;	}).filter(function(t) { return t !== ''; }); }; // Get link text for a wikiink to the discussion - including anchor, except for AfDs/MfDs Discussion.prototype.getNomPageLink = function { if (config.xfd.type === 'afd' || config.xfd.type === 'mfd') { return this.nomPage; } else { return this.nomPage + '#' + mw.util.wikiUrlencode(this.sectionHeader).replace(/_/g, ' '); } }; // Get nomination subpage Discussion.prototype.getNomSubpage = function { return this.nomPage.replace(config.xfd.subpagePath, ''); }; // Get page object by matching the title Discussion.prototype.getPageByTitle = function(title, options) { var convertModuleDoc = ( options && options.moduledocs && title.indexOf('Module:') === 0 ); var titleToCheck = ( convertModuleDoc ) ? title.replace(/\/doc$/,'') : title;

var search = mw.Title.newFromText(titleToCheck).getPrefixedText; for ( var i=0; i<this.pages.length; i++ ) { if ( search === this.pages[i].getPrefixedText ) { return this.pages[i]; }	}	return false; }; // Get page object by matching the talkpage's title Discussion.prototype.getPageByTalkTitle = function(t) { var search = mw.Title.newFromText(t).getPrefixedText; for ( var i=0; i')		.attr('title', 'Close discussion...')		.text('Close'),		']'	) .click(function {		return self.openDialog && self.setStatus('Closing...');	}); // Relist link var $relist = $(' ') .addClass('xfdc-action') .append(		'[',		$('<a>')		.attr({title:'Relist discussion...', class:'XFDcloser-link-relist'})		.text('Relist'),		']'	) .click(function {		return self.openDialog(true) && self.setStatus('Relisting...');	}); // quickKeep var $qk = $('<a>') .attr('title', 'quickKeep: close as "keep", remove nomination templates, '+		'add old xfd templates to talk pages') .text('qK') .click(function{		var inputData = new InputData(self);		inputData.result = 'keep';		inputData.after = 'doKeepActions';		self.setStatus('Closing...');		self.taskManager = new TaskManager(self, inputData);		self.taskManager.start;	});

// quickDelete var $qd = ( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $('<a>') .attr({		'title': 'quickDelete: close as "delete", delete nominated pages & their talk pages'+			(( config.xfd.type === 'rfd' ) ? '' :' & redirects')+			(( config.xfd.type === 'afd' || config.xfd.type === 'ffd' ) ? ', optionally '+ 'unlink backlinks' : ''),		'class': 'xfdc-qd'		}) .text('qD'); if ( !config.user.isSysop && config.xfd.type == 'tfd' ) { $qd.attr('title', 'quickDelete: close as "delete", tag nominated templates with '+			', add nominated templates to the holding cell as "orphan"') .click(function{			var inputData = new InputData(self);			inputData.result = 'delete';			inputData.after = 'holdingCell';			inputData.holdcell = 'orphan';			inputData.dontdeletetalk = true;			self.setStatus('Closing...');			self.taskManager = new TaskManager(self, inputData);			self.taskManager.start;		}); } else if ( config.user.isSysop ) { $qd.click(function{			$.when(config.xfd.type === 'tfd' ? multiButtonConfirm({					title: 'Really delete?',					message: 'Deletion will not remove transclusions from articles. Do you want to use the holding cell instead?',					actions: [						{ label: 'Cancel', flags: 'safe' },						{ label: 'Delete', flags: 'destructive', action: 'delete' },						{ label: 'Holding cell', flags: 'progressive', action: 'holdcell' }					],					size: 'medium'				}) : 'delete' )			.then(function(action) { var inputData = new InputData(self); inputData.result = 'delete'; if ( action === 'delete' ) { inputData.after = 'doDeleteActions'; inputData.deleteredir = ( config.xfd.type === 'rfd' ) ? null : true; inputData.unlinkbackl = ( config.xfd.type === 'afd' || config.xfd.type === 'ffd' ) ? true : null; } else if ( action === 'holdcell' ) { inputData.after = 'holdingCell'; inputData.holdcell = 'orphan'; inputData.dontdeletetalk = true; } else { // User selected Cancel return; }				self.setStatus('Closing...'); self.taskManager = new TaskManager(self, inputData); self.taskManager.start; });		});	}	// quickClose links var $quick = $(' ') .addClass('xfdc-action') .css('font-size', '92%') .append(		'[',		$('<a>')			.attr('title', 'quickClose discussion...')			.text('quickClose')			.click(function{ $(this).hide.next.show; }),		$(' ')			.hide			.append( ' ',				$qk, " ",				$(' ').html('&middot;'), " ",				$qd, ' ',				$(' ')					.attr({title: 'Cancel', class: 'xfdc-qc-cancel'}) .html(" x ") .click(function{						$(this).parent.hide.prev.show;					}) ),		']'	);

//Add links in place of status self.setStatus([		$close,		( self.isBasicMode || config.xfd.type==='cfd' ) ?  : $quick,		$relist,		additonal || 	]); }; // Retrieve extra information - pages' existance, nomination date(s) Discussion.prototype.retrieveExtraInfo = function { // Preserve reference to discussion object var self = this; var pagesExistencesPromise = API.get( {			action: 'query',			titles: self.getPageTitles.join('|'),			prop: 'info',			inprop: 'talkid'		} ) .then(arrayFromResponsePages) .then(function(pages) {			pages.forEach(function(page) { var pageObject = self.getPageByTitle(page.title); if ( !pageObject ) { return $.Deferred.reject('Unexpacted title `'+page.title+'`'); }				var pageExists = page.pageid > 0; var talkExists = page.talkid > 0; setExistence(pageObject, pageExists); setExistence(pageObject.getTalkPage, talkExists); });			return true;		}); var nominationDatePromise = ( config.xfd.type !== "afd" && config.xfd.type !== "mfd" ) ? $.Deferred.resolve(self.nomPage.split(config.xfd.path)[1]) : API.get({			action: 'query',			titles: self.nomPage,			prop: 'revisions',			rvprop: 'timestamp',			rvdir: 'newer',			rvlimit: '1'		}) .then(pageFromResponse) .then(function(page) {			var revisionDate = new Date(page.revisions[0].timestamp);			return dmyDateString(revisionDate);		}); nominationDatePromise.then(function(nomDate) {		self.nomDate = nomDate;		// For an RfD with no first comment date detected, use the nom page date in dmy format		if ( config.xfd.type === "rfd" && !self.firstDate ) {			self.firstDate = nomDate.replace(/(\d+) (\w*) (\d+)/g, "$3 $2 $1");		}	}); return $.when(pagesExistencesPromise, nominationDatePromise).then(		function{ return ''; },		function(failMessage, jqxhr) {			return $(' ').addClass('xfdc-notice-error').append( 'Error retrieving page information (reload the page to try again) ', $(' ').addClass('xfdc-notice-error').append(					extraJs.makeErrorMsg(failMessage, jqxhr)				) );		}	); };

// Check if discussion is in 'basic' mode - i.e. no pages Discussion.prototype.isBasicMode = function { return !this.pages || this.pages.length === 0; };

/* ========== Dialog class ====================================================================== The user interface for closing/relisting XfD discussions -- */ // Constructor var Dialog = function(discussion, relist) { this.discussion = discussion; this.relisting = !!relist; // Make an new, empty, not-displayed 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( 'XFDcloser' ); this.interfaceWindow.setScriptName('(v' + config.script.version + ')'); this.interfaceWindow.addFooterLink('script documentation', 'WP:XFDC'); this.interfaceWindow.addFooterLink('feedback', 'WT:XFDC'); this.interfaceWindow.setContent(		$(' ')		.attr('id', 'xfdc-dialog')		.append( $(' ').attr('id', 'xfdc-dialog-header'), $(' ').attr('id', 'xfdc-dialog-body'), $(' ').attr('id', 'xfdc-dialog-footer') )		.get(0)	); $(this.interfaceWindow.content).parent.parent.find('a.ui-dialog-titlebar-close.ui-corner-all').remove; $('#xfdc-dialog').parent.css('background-color', '#f0f0f0'); };

// -- Rcat multiselect widget --- */

/** * Dialog.RcatMultiselect * * Creates a modified OOUI MenuTagMultiselectWidget with Rcat templates as options, * grouped as per https://en.wikipedia.org/wiki/Template:R_template_index * * @requires {Modules} oojs-ui-core, oojs-ui-widgets * @param {function} onChangeCallback({String[]} datas) * Callback function for when the selected rcats change, is passed an array of Strings * each of which is a selected rcat template (including  braces ``). When all * prior selections are removed, an empty array is passed. * @param {object} context * The context to be used as the `this` value when executing the callback function * @param {jQuery} overlay * Overlay element. If ommited, element with id `ejs-rcats-overlay` will be emptied and used * if it exists, or else a div with that id will be created and used. * @return {LookupMenuTagMultiselectWidget} */ Dialog.RcatMultiselect = function(onChangeCallback, context, overlay) {

// Extend OO.ui.MenuSelectWidget to use a more intuitive lookup system, such that // e.g. typing "section" brings up `` as a match var LookupMenuSelectWidget = function(config) { OO.ui.MenuSelectWidget.call(this, config); };	LookupMenuSelectWidget.prototype = Object.create(OO.ui.MenuSelectWidget.prototype); LookupMenuSelectWidget.prototype.getItemMatcher = function ( s, exact ) { var re; if ( s.normalize ) { s = s.normalize; }		s = exact ? s.trim : s.replace( /^\s+/, '' ); re = s.replace( /([\\{}|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' ); if ( exact ) { re = '^\\s*' + re + '\\s*$'; }		re = new RegExp( re, 'i' ); return function ( item ) { var matchText = item.getMatchText; if ( matchText.normalize ) { matchText = matchText.normalize; }			return re.test( matchText ); };	};	// Extend OO.ui.MenuTagMultiselectWidget to use a LookupMenuSelectWidget instead of	// the standard MenuSelectWidget var LookupMenuTagMultiselectWidget = function ( config ) { OO.ui.MenuTagMultiselectWidget.call(this, config); // Override menu widget this.menu = new LookupMenuSelectWidget( $.extend( {				widget: this, input: this.hasInput ? this.input : null, $input: this.hasInput ? this.input.$input : null, filterFromInput: !!this.hasInput, $autoCloseIgnore: this.hasInput ? this.input.$element : $( [] ), $floatableContainer: this.hasInput && this.inputPosition === 'outline' ? this.input.$element : this.$element, $overlay: this.$overlay, disabled: this.isDisabled },			config.menu ) );		this.menu.connect( this, {			choose: 'onMenuChoose',			toggle: 'onMenuToggle'		} ); if ( this.hasInput ) { this.input.connect( this, { change: 'onInputChange' } ); }		if ( this.$input ) { this.$input.prop( 'disabled', this.isDisabled ); this.$input.attr( {				role: 'combobox',				'aria-owns': this.menu.getElementId,				'aria-autocomplete': 'list'			} ); }		if ( !this.popup ) { this.$content.append( this.$input ); this.$overlay.append( this.menu.$element ); }		//this.onMenuItemsChange; };	LookupMenuTagMultiselectWidget.prototype = Object.create(OO.ui.MenuTagMultiselectWidget.prototype); // Define new overlay, or use/reuse already defined element if ( overlay == null ) { if ( $('#ejs-rcats-overlay').length ) { overlay = $('#ejs-rcats-overlay').first.empty; } else { overlay = $(' ') .attr('id', 'ejs-rcats-overlay') .css({'z-index':'2000', 'position':'fixed', 'top':'0px', 'font-size':'85%'}) .prependTo('body'); }	}	// Make menu option widgets based on template name var newRcatOption = function(val) { return new OO.ui.MenuOptionWidget( {data: , label: } ); };	// Make the widget var rcatMultiselectWidget = new LookupMenuTagMultiselectWidget( {		allowArbitrary: true,		$overlay: overlay,		popup: false,		menu: {			items: [				// Common Rcats				new OO.ui.MenuSectionOptionWidget({label:'Common'}),					newRcatOption('R to related topic'),					newRcatOption('R from subtopic'),					newRcatOption('R to list entry'),					newRcatOption('R to section'),				new OO.ui.MenuSectionOptionWidget({label:'Related information'}),					newRcatOption('R from album'),					newRcatOption('R to article without mention'),					newRcatOption('R from book'),					newRcatOption('R to decade'),					newRcatOption('R from domain name'),					newRcatOption('R from top-level domain'),					newRcatOption('R from film'),					newRcatOption('R from gender'),					newRcatOption('R from list topic'),					newRcatOption('R from member'),					newRcatOption('R to related topic'),					newRcatOption('R from related word'), newRcatOption('R from phrase'), newRcatOption('R from school'), newRcatOption('R from song'), newRcatOption('R from subtopic'), newRcatOption('R to subtopic'), newRcatOption('R from Unicode'), new OO.ui.MenuSectionOptionWidget({label:'Fiction'}), newRcatOption('R from fictional character'), newRcatOption('R from fictional element'), newRcatOption('R from fictional location'), newRcatOption('R to TV episode list entry'), // Grammar, punctuation, and spelling new OO.ui.MenuSectionOptionWidget({label:'Abbreviation'}), newRcatOption('R to acronym'), newRcatOption('R from acronym'), newRcatOption('R to initialism'), newRcatOption('R from initialism'), new OO.ui.MenuSectionOptionWidget({label:'Capitalisation'}), newRcatOption('R from CamelCase'), newRcatOption('R from other capitalisation'), newRcatOption('R from miscapitalisation'), new OO.ui.MenuSectionOptionWidget({label:'Grammar & punctuation'}), newRcatOption('R from modification'), newRcatOption('R from plural'), newRcatOption('R to plural'), new OO.ui.MenuSectionOptionWidget({label:'Parts of speech'}), newRcatOption('R from adjective'), newRcatOption('R from adverb'), newRcatOption('R from common noun'), newRcatOption('R from gerund'), newRcatOption('R from proper noun'), newRcatOption('R from verb'), new OO.ui.MenuSectionOptionWidget({label:'Spelling'}), newRcatOption('R from alternative spelling'), newRcatOption('R from ASCII-only'), newRcatOption('R to ASCII-only'), newRcatOption('R from diacritic'), newRcatOption('R to diacritic'), newRcatOption('R from misspelling'), newRcatOption('R from stylization'), // Alternative names new OO.ui.MenuSectionOptionWidget({label:'Alternative names (general)'}), newRcatOption('R from alternative language'), newRcatOption('R from alternative name'), newRcatOption('R from former name'), newRcatOption('R from historic name'), newRcatOption('R from incorrect name'), newRcatOption('R from long name'), newRcatOption('R from portmanteau'), newRcatOption('R from short name'), newRcatOption('R from sort name'), newRcatOption('R from less specific name}'), newRcatOption('R from more specific name'), newRcatOption('R from synonym'), newRcatOption('R from antonym'), new OO.ui.MenuSectionOptionWidget({label:'Alternative names (people)'}), newRcatOption('R from birth name'), newRcatOption('R from given name'), newRcatOption('R to joint biography'), newRcatOption('R from married name'), newRcatOption('R from name with title'), newRcatOption('R from personal name'), newRcatOption('R from pseudonym'), newRcatOption('R from surname'), new OO.ui.MenuSectionOptionWidget({label:'Alternative names (technical)'}), newRcatOption('R from Java package name'), newRcatOption('R from molecular formula'), newRcatOption('R from technical name'), newRcatOption('R to technical name'), newRcatOption('R from trade name'), new OO.ui.MenuSectionOptionWidget({label:'Alternative names (organisms)'}), newRcatOption('R from scientific name'), newRcatOption('R from alternative scientific name'), newRcatOption('R to scientific name'), new OO.ui.MenuSectionOptionWidget({label:'Alternative names (geography)'}), newRcatOption('R from name and country'), newRcatOption('R from more specific geographic name'), newRcatOption('R from postal code'), // Navigation aids new OO.ui.MenuSectionOptionWidget({label:'Navigation'}), newRcatOption('R to anchor'), newRcatOption('R avoided double redirect'), newRcatOption('R from file metadata link'), newRcatOption('R to list entry'), newRcatOption('R mentioned in hatnote'), newRcatOption('R to section'), newRcatOption('R from shortcut'), newRcatOption('R from template shortcut'), new OO.ui.MenuSectionOptionWidget({label:'Disambiguation'}), newRcatOption('R from ambiguous term'), newRcatOption('R to anthroponymy page'), newRcatOption('R to disambiguation page'), newRcatOption('R from incomplete disambiguation'), newRcatOption('R from incorrect disambiguation'), newRcatOption('R from other disambiguation'), newRcatOption('R from unnecessary disambiguation'), new OO.ui.MenuSectionOptionWidget({label:'Merge, duplicate & move'}), newRcatOption('R from duplicated article'), newRcatOption('R with history'), newRcatOption('R from merge'), newRcatOption('R from move'), newRcatOption('R with old history'), new OO.ui.MenuSectionOptionWidget({label:'To namespaces'}), newRcatOption('R to category namespace'), newRcatOption('R to draft namespace'), newRcatOption('R to help namespace'), newRcatOption('R to main namespace'), newRcatOption('R to portal namespace'), newRcatOption('R to project namespace'), newRcatOption('R to talk page'), newRcatOption('R to template namespace'), newRcatOption('R to user namespace'), // Miscellaneous new OO.ui.MenuSectionOptionWidget({label:'ISO codes'}), newRcatOption('R from ISO 4'), newRcatOption('R from ISO 639 code'), newRcatOption('R from ISO 3166 code'), newRcatOption('R from ISO 4217 code'), newRcatOption('R from ISO 15924 code'), new OO.ui.MenuSectionOptionWidget({label:'Miscellaneous'}), newRcatOption('R printworthy'), newRcatOption('R unprintworthy'), newRcatOption('Wikidata redirect') ]		}	} );	// Execute callback when selector data changes	rcatMultiselectWidget.on('change', function(selectedOptions) { onChangeCallback.call(			context,			selectedOptions.map(function(optionWidget) { return optionWidget.data; })		);	});	return rcatMultiselectWidget; };

// -- Dialog prototype -- */

// --- Basic manipulation: --- // Append content to header Dialog.prototype.addToHeader = function($content) { $('#xfdc-dialog-header').append($content); }; // Append content to body Dialog.prototype.addToBody = function($content) { $('#xfdc-dialog-body').append($content); }; // Append content to footer Dialog.prototype.addToFooter = function($content) { $('#xfdc-dialog-footer').append($content); }; // Clear dialog Dialog.prototype.emptyContent = function { $('#xfdc-dialog-header, #xfdc-dialog-body, #xfdc-dialog-footer').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; $('#ejs-rcats-overlay').remove; };

// --- Make interface elements: --- Dialog.prototype.makeHeader = function(multimode, isRelisting) { var self = this; // Title, pagecount var $header = $(' ') .attr('id', 'closeXFD-interface-header') .append(		$(' ')		.attr('id', 'closeXFD-interface-header-action')		.append( ( self.relisting ) ? 'Relisting discussion ' : 'Closing discussion ', $(' ').text(self.discussion.sectionHeader), ' ',			$(' ')			.attr({'id':'closeXFD-pagecount', 'class':'xfdc-bracketed'}) .css('font-size', '80%') .append(				$(' ')					.attr('id', 'closeXFD-pagecount-pages')					.css({'background':'#ff8', 'padding':'1px'})					.text( ( self.discussion.isBasicMode || ( config.xfd.type === 'cfd' && !self.relisting ) ) ? 'basic mode only' : self.discussion.pages.length + ( ( self.discussion.pages.length === 1 ) ? ' page' : ' pages' ) ),				( multimode ) ?  : ' ',				( multimode ) ?  : $('<a>').attr('id', 'closeXFD-pagecount-show').text('[show]'),				( multimode ) ? '' : $('<a>').attr('id', 'closeXFD-pagecount-hide').text('[hide]').hide			) )	);	$header.find('#closeXFD-pagecount-show, #closeXFD-pagecount-hide').click(function {		$('#closeXFD-nominatedpages, #closeXFD-pagecount-show, #closeXFD-pagecount-hide').toggle;	}); // Multi/single-mode button if ( !self.relisting && !self.discussion.isBasicMode && self.discussion.pages.length > 1 && config.xfd.type !== 'cfd' ) { $header.prepend(			$(' ')			.css('float', 'right')			.text(( multimode ) ? 'Single result...' : 'Multiple results...')			.click(function { if ( multimode ) { self.setup; } else { self.setupForMultiClose; }			})		);	}	return $header; };

Dialog.prototype.makePagesList = function(multimode) { var self = this;

// Template for per-page actions for multimode var $action_dropdown = $(' ') .attr('class', 'closeXFD-pagelist-actionDropdown') .css('margin-right', '0.5em') .append(		$(' ').attr('value', 'default').text(),		$(' ').attr('value', 'keep').text('Keep'),		( config.xfd.type === 'ffd' ) ?  : $(' ')			.attr('value', 'redirect').text('Redirect'),		( config.xfd.type === 'ffd' || config.xfd.type === 'rfd' ) ?  : $(' ')			.attr('value', 'merge').text('Merge'),		( config.xfd.type !== 'rfd' ) ?  : $(' ')			.attr('value', 'disambig').text('Disambiguate'),		$(' ').attr('value', 'no consensus').text('No consensus'),		$(' ').attr('value', 'del').text('Delete'),		$(' ').attr('value', 'na').text('(no action)')	); var $target = $(' ') .addClass('closeXFD-pagelist-target') .append(		' to:',		$(' ').attr('type', 'text')	) .css('display', 'none');

// List of pages (or info on basic mode) var isCfdClose = config.xfd.type === 'cfd' && !self.relisting; var $pagesList = $('<ul>') .attr('id', 'closeXFD-nominatedpages') .css('font-size', '95%'); if ( !self.discussion.isBasicMode && !isCfdClose ) { for ( var i=0; i<self.discussion.pages.length; i++ ) { var pageTitle = self.discussion.pages[i].getPrefixedText; $('<li>') .append(				( !multimode ) ?  : $action_dropdown					.clone					.attr('id', 'closeXFD-pagelist-action-'+i)					.data('pageTitle', pageTitle),				$(' ').addClass('closeXFD-pagelist-title').text(pageTitle),				' ',				( !multimode ) ?  : $target					.clone					.attr('id', 'closeXFD-pagelist-target-'+i)					.data('pageTitle', pageTitle)			) .appendTo($pagesList); if ( multimode ) { self.inputData.pageActions[pageTitle] = { 'id':'closeXFD-pagelist-action-'+i }; }		}	} else { $('<li>') .append(			( !self.discussion.isBasicMode && isCfdClose )				? 'Automated post-close actions are not available for CFDs.'				: 'Nominated pages were not detected.',			'You can still ',			( self.relisting ) ? 'relist' : 'close',			' this ',			config.xfd.type.toUpperCase,			' discussion, but updating the nomination templates will need to be done manually '+			'– see ',			extraJs.makeLink('WP:'+config.xfd.type.toUpperCase+'AI'),			' for instructions.'		) .appendTo($pagesList); }	// On changing a multimode action, show/hide corresponding options as appropriate $pagesList.find('select').change(function{		if ( $(this).children.first.val === 'default' ) {			$(this).children.first.remove;		}		var $li = $(this).parent;		var v = $(this).val;		var t = $li.find('.closeXFD-pagelist-title').text;		self.inputData.pageActions[t].action = v;		if ( v === 'redirect' || v === 'merge' ) {			$li.find('.closeXFD-pagelist-target').show;		} else {			$li.find('.closeXFD-pagelist-target').val('').change.hide;		}		self.updateMultimodeOptions;	}); $pagesList.find('input').change(function{		var t = $(this).parent.parent.find('.closeXFD-pagelist-title').text;		self.inputData.pageActions[t].target = mw.Title.newFromText( $(this).val.trim );	});	if ( !multimode ) { $pagesList.hide; }	return $pagesList; };

Dialog.prototype.makeNonAdminNote = function { var $NACD_note = $(' ') .css({'text-align':'center', 'clear':'both'}) .append(		$(' ')		.append( 'See the ', extraJs.makeLink('WP:NACD'), ' guideline for advice on appropriate and inappropriate closures' ),		$(' ')	);	return $NACD_note; };

Dialog.prototype.makeCloseResult = function { var self = this; var $resultContainer = $(' ') .attr('id', 'closeXFD-resultContainer') .css({'float':'left', 'width':'99%', 'padding-bottom':'1%'}) .append(		$(' ').append( $(' ').text('Result'), // Speedy $(' ').addClass('xfdc-dialog-bracketedOption').append(				$(' ')				.attr({'type':'checkbox', 'name':'closeXFD-speedy', 'id':'closeXFD-speedy'})				.change(function{ self.inputData.speedy = ( $(this).prop('checked') ); }),				$(' ').attr('for', 'closeXFD-speedy').text('Speedy')			) ),		$(' ').attr('id', 'closeXFD-resultOptions').css('overflow','hidden').append( // Keep $(' ').attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-keepResult', 'id':'closeXFD-result-keep', 'value':'keep'}),				$(' ').attr('for', 'closeXFD-result-keep').text('Keep')			), // Redirect ( config.xfd.type === 'ffd' ) ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-keepResult', 'id':'closeXFD-result-redir', 'value':(( config.xfd.type === 'rfd' ) ? 'retarget' : 'redirect')}),				$(' ')				.attr('for', 'closeXFD-result-redir')				.text(( config.xfd.type === 'rfd' ) ? 'Retarget' : 'Redirect'),				( !config.user.isSysop ) ? '' : $(' ')				.attr('for', 'closeXFD-result-redir')				.text(( config.xfd.type === 'rfd' ) ? 'Delete and retarget' : 'Delete and redirect')				.hide			), // Soft redirect ( config.xfd.type !== 'rfd' ) ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-keepResult', 'id':'closeXFD-result-softredir', 'value':'soft redirect'}),				$(' ').attr('for', 'closeXFD-result-softredir').text('Soft redirect'),				( !config.user.isSysop ) ? '' : $(' ')				.attr('for', 'closeXFD-result-softredir').text('Delete and soft redirect').hide			), // No consensus $(' ').attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-keepResult', 'id':'closeXFD-result-nocon', 'value':'no consensus'}),				$(' ').attr('for', 'closeXFD-result-nocon').text('No consensus')			), // Merge ( config.xfd.type === 'ffd' || config.xfd.type === 'rfd' ) ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'id':'closeXFD-result-merge', 'value':'merge'}),				$(' ').attr('for', 'closeXFD-result-merge').text('Merge')			), // Disambiguate ( config.xfd.type !== 'rfd' ) ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'id':'closeXFD-result-disambig', 'value':'disambiguate'}),				$(' ').attr('for', 'closeXFD-result-disambig').text('Disambiguate')			), // Delete ( !config.user.isSysop && config.xfd.type !== 'tfd') ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete', 'value':'delete'}),				$(' ').attr('for', 'closeXFD-result-delete').text('Delete')			), // Delete (disabled) ( config.user.isSysop || config.xfd.type === 'tfd') ? '' : $(' ')			.attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete-disabled', 'disabled':'disabled'}),				extraJs.addTooltip( $(' ')						.attr('for', 'closeXFD-result-delete-disabled') .text('Delete '), 'Non-admin closure is not appropriate when the result will require action by an administrator (per WP:BADNAC)' )			),			// Soft delete ( !config.user.isSysop || config.xfd.type === 'rfd' ) ? '' : $(' ')			.attr({'class':'xfdc-dialog-option', 'id':'closeXFD-resultContainer-softdel'}).append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'class':'closeXFD-deleteResult', 'id':'closeXFD-result-softdel', 'value':'soft delete'}),				$(' ').attr('for', 'closeXFD-result-softdel').text('Soft delete')			), // Custom $(' ').attr('class', 'xfdc-dialog-option').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-result', 'id':'closeXFD-result-custom', 'value':'custom'}),				$(' ').attr('for', 'closeXFD-result-custom').text('Custom')			) ),		$(' ')		.attr({'id':'closeXFD-resultContainer-custom'})		.css({'clear':'both', 'padding-bottom':'1%', 'display':'none'})		.append( $(' ').attr('for', 'closeXFD-result-custom-input').text('Custom result: '), $(' ')			.attr({'type':'text', 'name':'closeXFD-result-custom-input',				'id':'closeXFD-result-custom-input'}) .change(function{				self.inputData.customResult = $('#closeXFD-result-custom-input').val.trim;			}) ),		// Target page		$(' ')		.attr({'id':'closeXFD-resultContainer-target'})		.css({'clear':'both', 'padding-bottom':'1%', 'display':'none'})		.append( $(' ').attr({'id':'closeXFD-result-target-label', 'for':'closeXFD-result-target'}) .text('Target:'), $(' ')			.attr({'type':'text', 'name':'closeXFD-result-target', 'id':'closeXFD-result-target'}) .change(function{				self.inputData.target = mw.Title.newFromText( $('#closeXFD-result-target').val.trim );			})		)	);	$resultContainer.find('span.xfdc-dialog-option').after(' '); $resultContainer.find("input[type=radio][name='closeXFD-result']").change(function{		var v = $(this).val;		self.inputData.result = v;		var currentAfter = $('input[name="closeXFD-after"]:checked').val || false;		// Show/hide options		switch(v) {			default:			case 'keep':			case 'no consensus':				$('.closeXFD-keepOptions').show;				$('.closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+ '#closeXFD-customOptions, #closeXFD-resultContainer-target, '+ '#closeXFD-resultContainer-custom').hide;				// default options:				if ( !currentAfter || ( currentAfter !== 'doKeepActions' && currentAfter !== 'noAction' ) ) {					$('#closeXFD-after-doKeepActions').prop('checked', true).change;				}				break;			case 'retarget':			case 'redirect':			case 'soft redirect':				$('.closeXFD-redirectOptions, #closeXFD-resultContainer-target').show;				$('.closeXFD-keepOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+ '#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide;				$('#closeXFD-result-target-label').text(extraJs.toSentenceCase(v) + ' to: ');				// default options:				if ( !currentAfter || ( currentAfter !== 'doRedirectActions' && currentAfter !== 'noAction' ) ) {					$('#closeXFD-after-doRedirectActions')					.prop('checked', true).change;				}				break;			case 'merge':				$('.closeXFD-mergeOptions, #closeXFD-resultContainer-target').show;				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+ '#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide;				$('#closeXFD-result-target-label').text('Merge to: ');				// default options:				if ( !currentAfter || ( currentAfter !== 'doMergeActions' && currentAfter !== 'noAction' ) ) {					$('#closeXFD-after-doMergeActions').prop('checked', true).change;				}				break;			case 'disambiguate':				$('.closeXFD-disambigOptions').show;				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-deleteOptions, '+ '#closeXFD-customOptions, #closeXFD-resultContainer-target, '+ '#closeXFD-resultContainer-custom').hide;				// default options:				if ( !currentAfter || ( currentAfter !== 'doDisambigActions' && currentAfter !== 'noAction' ) ) {					$('#closeXFD-after-doDisambigActions').prop('checked', true).change;				}				break;			case 'delete':			case 'soft delete':				$('.closeXFD-deleteOptions').show;				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, '+ '#closeXFD-customOptions, #closeXFD-resultContainer-target, '+ '#closeXFD-resultContainer-custom').hide;				// default options:				if ( !currentAfter || ( currentAfter !== 'doDeleteActions' &&					currentAfter !== 'holdingCell' &&					currentAfter !== 'noAction' ) ) {					$('#closeXFD-after-' + (( config.xfd.type === 'tfd' ) ? 'holdingCell' : 'doDeleteActions' ) + ', #closeXFD-del-deleteredir, #closeXFD-del-unlinkbackl')					.prop('checked', true).change;				}				// When soft delete is selected, prepend rationale with REFUND message				if ( v === 'soft delete' ) {					var old_rationale = $('#closeXFD-rationale').val;					if (!old_rationale.includes("Soft deletion rationale")) {						$('#closeXFD-rationale').val( "" +	self.discussion.pages[0].getPrefixedText + " "								: "}} " ) +							old_rationale						) .change; }				}				break; case 'custom': $('#closeXFD-customOptions, #closeXFD-resultContainer-custom, '+				'.closeXFD-keepOptions, .closeXFD-deleteOptions').show; $('.closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, '+				'#closeXFD-resultContainer-target').hide; // default options: if ( !currentAfter || currentAfter !== 'noAction' ) { $('#closeXFD-after-doKeepActions').prop('checked', true).change; }		}		self.resetHeight; });	return $resultContainer;

};

Dialog.prototype.makeRationaleBox = function(multimode) { var self = this; var $rationale = $(' ') .css('clear', 'both') .append(		$(' ').attr('id', 'closeXFD-rationale-label')		.text( ( self.relisting ) ? 'Relist comment' : (( multimode ) ? 'Rationale' : 'Additional '+			'rationale')),		( !multimode ) ? ' (optional):' : $('<a>')		.addClass('xfdc-dialog-bracketedOption')		.text('copy from above')		.click(function{ var results = ''; for ( var p in self.inputData.pageActions ) { var a = self.inputData.pageActions[p].action; if ( a ) { a = a					.replace('del', 'delete') .replace(/(default|na)/, ' '); } else { a = ' '; }				var t = ( /(merge|redirect)/.test(a) ) ? ' to ' +					self.inputData.pageActions[p].target + '\n' : '\n'; results += "*" +	extraJs.toSentenceCase(a) + " " + p + "" + t;			} $('#closeXFD-rationale').val(results + self.inputData.rationale).change; }),		$(' '),		$(' ')		.attr({'id':'closeXFD-rationale', 'rows':(( self.relisting ) ? 2 : 4)})		.css('width', '99%')		.change(function{ self.inputData.rationale = $(this).val.trim; })		.change,		( self.relisting ) ? '' : $(' ')		.css({'clear':'both', 'padding-bottom':'1%'})		.append( $(' ')			.attr({'type':'checkbox', 'name':'closeXFD-rationale-sentence',				'id':'closeXFD-rationale-sentence'}) .change(function{				if ( $(this).prop('checked') ) {					self.inputData.resultPunct = '';				} else {					self.inputData.resultPunct = '.';				}			}) .change, $(' ').attr('for', 'closeXFD-rationale-sentence') .text('Rationale is not a new sentence') )	);	if ( multimode ) { $(' ')		.css({'clear':'both', 'padding-bottom':'1%'}) .append(			extraJs.addTooltip( $(' ').attr('for', 'closeXFD-resultSummary-input').text('Result summary: '), 'Appears in bold text before the rationale; also used in edit summaries, and in'+ ' Old ' + config.xfd.type + ' templates' ),			$(' ')			.attr({ 'type':'text', 'name':'closeXFD-resultSummary-input', 'id':'closeXFD-resultSummary-input' })			.css({"margin-left": "0.5em"})			.change(function{ self.inputData.result = $(this).val.trim; })		)		.prependTo($rationale); }	return $rationale; };

Dialog.prototype.makeAfterActions = function(multimode) { var self = this;

var $after = $(' ') .attr('id', 'closeXFD-afterContainer') .addClass('xfdc-dialog-container') .css('max-width', ( multimode ) ? '30%' : '40%') .append(		$(' ').text('After closing:').css('display','block'),		// KeepActions		$(' ')		.addClass('closeXFD-keepOptions')		.hide		.append( ( multimode ) ? $(' ')			.text('"Keep"/"No consensus" pages: ') : $(' ') .attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-doKeepActions',				'value':'doKeepActions'			}), extraJs.addTooltip(				$(( multimode ) ? ' ' : ' ')					.attr('for', 'closeXFD-after-doKeepActions')					.text('Update pages and talk pages '),				'Remove nomination templates, and add old xfd templates to talk pages'			) ),		// RedirectActions		$(' ')		.addClass('closeXFD-redirectOptions')		.hide		.append( ( multimode ) ? $(' ')			.text('"Redirect" pages: ') : $(' ').attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-doRedirectActions',				'value': 'doRedirectActions'			}), extraJs.addTooltip(				$(( multimode ) ? ' ' : ' ')					.attr('for', 'closeXFD-after-doRedirectActions')					.text('Redirect pages and update talk pages '),				'Redirect nominated pages to the specified target, '+					'and add old xfd templates to talk pages'			) ),		// MergeActions		$(' ')		.addClass('closeXFD-mergeOptions')		.hide		.append( ( multimode ) ? $(' ')			.text('"Merge" pages: ') : $(' ').attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-doMergeActions',				'value': 'doMergeActions'			}), extraJs.addTooltip(				$(( multimode ) ? ' ' : ' ')					.attr('for', 'closeXFD-after-doMergeActions')					.text('Update pages and talk pages '),				'Replace nomination templates with merge templates, '+					'and add old xfd templates to talk pages'			) ),		// DisambigActions		$(' ')		.addClass('closeXFD-disambigOptions')		.hide		.append( ( multimode ) ? $(' ')			.text('"Disambiguate" pages: ') : $(' ') .attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-doDisambigActions',				'value':'doDisambigActions'			}), extraJs.addTooltip(				$(( multimode ) ? ' ' : ' ')					.attr('for', 'closeXFD-after-doDisambigActions')					.text('Update pages and talk pages '),				'Remove nomination templates, and add old xfd templates '+					'to talk pages'			) ),		// Delete actions		( multimode || !config.user.isSysop ) ? '' : $(' ')		.addClass('closeXFD-deleteOptions')		.hide		.append( $(' ').attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-doDeleteActions',				'value': 'doDeleteActions'			}), extraJs.addTooltip(				$(' ')					.attr('for', 'closeXFD-after-doDeleteActions')					.text('Delete pages '),				'Delete nominated pages and (unless otherwise specified) '+					'their talk pages'			) ),		// Holding cell		( multimode || config.xfd.type !== 'tfd' ) ? '' : $(' ')		.addClass('closeXFD-deleteOptions')		.hide		.append( $(' ').attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-holdingCell',				'value':'holdingCell'			}), extraJs.addTooltip(				$(' ')					.attr('for', 'closeXFD-after-holdingCell')					.text('List pages at holding cell '),				'Replace nomination template with, '+					'and list at the specified holding cell section'			) ),		// Multimode delete/holding cell		( !multimode ) ? '' : $(' ')		.addClass('closeXFD-deleteOptions')		.hide		.append( $(' ').text('"Delete" pages: '), ( !config.user.isSysop ) ? '' : extraJs.addTooltip(				$(" ").text('Delete pages '),				'Delete nominated pages and (unless otherwise specified) their talk pages'			), ( config.xfd.type !== 'tfd' || !config.user.isSysop ) ? '' : ' or ', ( config.xfd.type !== 'tfd' ) ? '' : extraJs.addTooltip(				$(" ").text( (( config.user.isSysop ) ? 'l' : 'L') + 'ist pages at holding cell' ),				'Replace nomination template with, and list at the specified holding cell section'			) ),		// No actions		$(' ')		.attr('id', 'closeXFD-noAction')		.toggle(!multimode)		.append( ( multimode ) ? $(' ')			.text('"(no action)" pages: ') : $(' ').attr({				'type':'radio',				'name':'closeXFD-after',				'id':'closeXFD-after-noAction',				'value':'noAction'			}), $(( multimode ) ? ' ' : ' ') .attr('for', 'closeXFD-after-noAction').text('No automated actions') )	)	.find('input').change(function {		self.inputData.after = $(this).val;		// Hide options if no action selected, or 'doKeepActions' is selected		$('#closeXFD-optionsContainer').toggle(!/(noAction|doKeepActions)/.test(self.inputData.after));		// Show or hide holding cell sections and 'delete redirects', if holding cell selected		if ( config.xfd.type === 'tfd' ) {			$('#closeXFD-del-holdcell').toggle(self.inputData.after === 'holdingCell');			$('#closeXFD-del-deleteredir').parent			.toggle(self.inputData.after === 'doDeleteActions');			// Disable 'don't delete talk' option for holding cell (except 'ready for deletion')			if ( self.inputData.after === 'holdingCell' && self.inputData.holdcell !== 'ready' ) {				$('#closeXFD-del-deletetalk').prop('disabled', true)				.next.addClass('xfdc-dialog-disabled');			} else {				$('#closeXFD-del-deletetalk').prop('disabled', false)				.next.removeClass('xfdc-dialog-disabled'); }		}	} )	.end;	if ( multimode ) {		$after.children('div').addClass('xfdc-dialog-actionInfo');	}	return $after;

};

Dialog.prototype.makeOptions = function(multimode) { var self = this; var $options = $(' ') .attr('id', 'closeXFD-optionsContainer') .append(		// Redirect options:		( config.xfd.type === 'ffd' ) ? '' : $(' ')		.addClass('closeXFD-redirectOptions')		.hide		.append( $(' ').text(( multimode ) ? 'Redirect options' : 'Options') .css('display','block'), // Delete before redirecting ( !config.user.isSysop ) ? '' : $(' ').append(				$(' ')				.attr({ 'type':'checkbox', 'name':'closeXFD-redir-deleteFirst', 'id':'closeXFD-redir-deleteFirst' })				.change(function{ self.inputData.deleteFirst = $(this).prop('checked'); if ( !multimode ) { // toggle result labels $('#closeXFD-result-redir').nextAll.toggle; $('#closeXFD-result-softredir').nextAll.toggle; }				}),				$(' ').attr('for', 'closeXFD-redir-deleteFirst')				.text('Delete before redirecting')			), // Rcats $(' ').append(				$(' ').attr({ 'type':'checkbox', 'name':'closeXFD-redir-addRcats', 'id': 'closeXFD-redir-rcats', 'disabled': true }).prop('checked', ( config.xfd.type === 'afd' )),				$(' ').attr('for', 'closeXFD-redir-rcats').append( extraJs.addTooltip(						$('<a>')							.attr({ 'href':'https://en.wikipedia.org/wiki/Template:R_template_index', 'target':'_blank' })							.text('Rcats: '),						'Full markup required, e.g. "". '+						'Multiple rcats can be specified, e.g. "'+						'". Leave blank if unsure which rcat to use.'					) ),				self.rcatSelector.$element.detach			) ),		// Merge options: 		( config.xfd.type !== 'tfd' ) ? '' : $(' ')		.attr('id', 'closeXFD-merge-holdcell')		.addClass('closeXFD-mergeOptions')		.hide		.append( $(' ').text('Holding cell merge subsection:').css('display','block'), // Infobox $(' ').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell', 'id':'closeXFD-holdingcell-merge-infobox', 'value':'merge-infobox'}),				$(' ').attr('for', 'closeXFD-holdingcell-merge-infobox').text('Merge (Infobox)')			), // Navigation template $(' ').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell', 'id':'closeXFD-holdingcell-merge-navigation', 'value':'merge-navigation'}),				$(' ').attr('for', 'closeXFD-holdingcell-merge-navigation')				.text('Merge (Navigation template)')			), // Link template $(' ').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell', 'id':'closeXFD-holdingcell-merge-link', 'value':'merge-link'}),				$(' ').attr('for', 'closeXFD-holdingcell-merge-link')				.text('Merge (Link template)')			), // Other $(' ').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell', 'id':'closeXFD-holdingcell-merge-other', 'value':'merge-other'}),				$(' ').attr('for', 'closeXFD-holdingcell-merge-other')				.text('Merge (Other)')			), // Meta $(' ').append(				$(' ').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell', 'id':'closeXFD-holdingcell-merge-meta', 'value':'merge-meta'}),				$(' ').attr('for', 'closeXFD-holdingcell-merge-meta')				.text('Merge (Meta)')			) )		.find("input[type=radio][name='closeXFD-merge-holdingcell']").change(function{ self.inputData.mergeHoldcell = $(this).val; })		.end,		// Deletion options		( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $(' ')		.addClass('closeXFD-deleteOptions')		.hide		.append( $(' ')			.text(( multimode ) ? 'Delete options' : 'Options').css('display','block'), // Don't delete talk pages ( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $(' ').append(				$(' ').attr({'type':'checkbox', 'name':'closeXFD-del-deletetalk', 'id':'closeXFD-del-deletetalk', 'value':'dontdeletetalk'}),				$(' ').attr('for', 'closeXFD-del-deletetalk').append( ( config.user.isSysop ) ? 'Do not delete talk pages' : 'Do not tag talk '+ 'pages for speedy deletion' )			),			// Delete redirects ( !config.user.isSysop || config.xfd.type === 'rfd' ) ? '' : $(' ').append(				$(' ').attr({'type':'checkbox', 'name':'closeXFD-del-deleteredir', 'id':'closeXFD-del-deleteredir', 'value':'deleteredir', 'checked':'checked'}),				$(' ').attr('for', 'closeXFD-del-deleteredir')				.text('Also delete redirects')			), // Unlink backlinks ( config.xfd.type !== 'afd' && config.xfd.type !== 'ffd' ) ? '' : $(' ')			.append(				$(' ').attr({'type':'checkbox', 'name':'closeXFD-del-unlinkbackl', 'id':'closeXFD-del-unlinkbackl', 'value':'unlinkbackl', 'checked':'checked'}),				$(' ').attr('for', 'closeXFD-del-unlinkbackl').text('Unlink backlinks')			), // Delete and redirect ( multimode || !config.user.isSysop || config.xfd.type === 'ffd' ) ? '' : $(' ')			.append(				// Hidden checkbox, so that text lines up				$(' ').attr('type', 'checkbox').css('visibility', 'hidden'),				// Shortcut for switching to 'Redirect' and selects 'Delete before redirecting'				$('<a>').attr('id', 'closeXFD-del-deleteAndRedir')				.text('Delete and redirect...')				.click(function{ $('#closeXFD-result-redir').prop('checked', true).change; $('#closeXFD-redir-deleteFirst').prop('checked', true); // Switch result labels to 'Delete and ...' versions, if needed if ( $('#closeXFD-result-redir').next.is(':visible') ) { $('#closeXFD-redir-deleteFirst').change; }				} )			),			// Holding cell ( config.xfd.type !== 'tfd' ) ? '' : $(' ')			.attr('id', 'closeXFD-del-holdcell') .append(				$(' ').css('display', 'block').append( ( !multimode ) ? 'Holding cell section:' : $(' ') .attr({'type':'checkbox', 'name':'closeXFD-del-useHoldingCell',						'id':'closeXFD-del-useHoldingCell', 'value':'useHoldingCell'}) .on('change', function{						self.inputData.useHoldingCell = $(this).prop('checked');						if ( self.inputData.useHoldingCell ) {							$('#closeXFD-del-deletetalk, #closeXFD-del-deleteredir')							.prop('disabled', true)							.next.addClass('xfdc-dialog-disabled');							$('#closeXFD-del-holdcell').children('div').show;						} else {							$('#closeXFD-del-deletetalk, #closeXFD-del-deleteredir')							.prop('disabled', false)							.next.removeClass('xfdc-dialog-disabled');							$('#closeXFD-del-holdcell').children('div').hide;						}					}) .prop('checked', !config.user.isSysop).change .prop('disabled', !config.user.isSysop) .toggle(config.user.isSysop), ( !multimode ) ? '' : $(' ')					.attr('for', 'closeXFD-del-useHoldingCell') .text((config.user.isSysop) ? 'Add to holding cell '+						'instead of deleting:' : 'Holding cell section:') ),				// Review				$(' ').append( $(' ').attr({'type':'radio', 'name':'closeXFD-holdingcell',						'id':'closeXFD-holdingcell-review', 'value':'review'}), $(' ').attr('for', 'closeXFD-holdingcell-review').text('Review') ),				// Convert				$(' ').append( $(' ').attr({'type':'radio', 'name':'closeXFD-holdingcell',						'id':'closeXFD-holdingcell-convert', 'value':'convert'}), $(' ').attr('for', 'closeXFD-holdingcell-convert').text('Convert') ),				// Substitute				$(' ').append( $(' ').attr({'type':'radio', 'name':'closeXFD-holdingcell',						'id':'closeXFD-holdingcell-substitute', 'value':'substitute'}), $(' ').attr('for', 'closeXFD-holdingcell-substitute').text('Substitute') ),							// Orphan				$(' ').append( $(' ').attr({'type':'radio', 'name':'closeXFD-holdingcell',						'id':'closeXFD-holdingcell-orphan', 'value':'orphan'}), $(' ').attr('for', 'closeXFD-holdingcell-orphan').text('Orphan') ),				// Ready for deletion				$(' ').append( $(' ').attr({'type':'radio', 'name':'closeXFD-holdingcell',						'id':'closeXFD-holdingcell-ready', 'value':'ready'}), $(' ').attr('for', 'closeXFD-holdingcell-ready') .text('Ready for deletion') )			)			.children('div').toggle(!multimode || !config.user.isSysop).end .find("input[type=radio][name='closeXFD-holdingcell']").change(function {				self.inputData.holdcell = $(this).val;				// Disable 'don't delete talk' option unless 'ready for deletion' selected				if ( self.inputData.holdcell !== 'ready' ) {					$('#closeXFD-del-deletetalk').prop('disabled', true)					.next.addClass('xfdc-dialog-disabled');				} else {					$('#closeXFD-del-deletetalk').prop('disabled', false)					.next.removeClass('xfdc-dialog-disabled');				}			} ) .end )	)	.find('input[type=checkbox]') .not('#closeXFD-redir-deleteFirst, #closeXFD-del-useHoldingCell') .change(function{			var v = $(this).val;			self.inputData[v] = $(this).prop('checked');		}) .change .end .end; if ( multimode ) { $options.children.addClass('xfdc-dialog-container'); } else { $options.css({'float':'right', 'width':'59%'}); }

return $options; };

Dialog.prototype.makeButtons = function(multimode) { var self = this; var $buttons = $(' ') .attr('id', 'closeXFD-interface-buttons') .css({'text-align':'center', 'clear':'both'}) .append(		$(' ')		.attr('id', 'closeXFD-interface-cancel')		.text('Cancel')		.click(function { self.close; self.discussion.showLinks; })	);	if ( !self.relisting ) { $buttons.prepend(			$(' ')			.attr('id', 'closeXFD-interface-closedisc')			.text('Close Discussion')			.click(function { if ( multimode ) { self.evaluateMultimodeClose; } else { self.evaluateClose; }			}),			$(' ')			.attr('id', 'closeXFD-interface-preview')			.text('Preview')			.click(function { if ( multimode ) { self.evaluateMultimodeClose(true); } else { self.evaluateClose(true); }			})		);	} else { $buttons.prepend(			$(' ').attr('id', 'closeXFD-interface-relistdisc').text('Relist Discussion')			.click(function { self.close; self.discussion.taskManager = new TaskManager(self.discussion); self.discussion.taskManager.start; }),			$(' ').attr('id', 'closeXFD-interface-previewRelist').text('Preview')			.click(function { self.showPreview(true); })		);	}

return $buttons; };

Dialog.prototype.makePreviewBox = function { return $(' ').attr('id', 'closeXFD-preview-output').hide; };

Dialog.prototype.updateMultimodeOptions = function { $('.closeXFD-keepOptions').toggle(this.inputData.inPageActions('keep') || this.inputData.inPageActions('no consensus')); $('.closeXFD-redirectOptions').toggle(this.inputData.inPageActions('redirect')); $('.closeXFD-mergeOptions').toggle(this.inputData.inPageActions('merge')); $('.closeXFD-disambigOptions').toggle(this.inputData.inPageActions('disambig')); $('.closeXFD-deleteOptions').toggle(this.inputData.inPageActions('del')); $('#closeXFD-noAction').toggle(this.inputData.inPageActions('na')); this.resetHeight; };

Dialog.prototype.storeRcatData = function(rcatData) { this.inputData.rcats = rcatData.join('\n').trim; $('#closeXFD-redir-rcats').prop('checked', rcatData.length > 0); };

// --- Set up for close or relist: --- Dialog.prototype.setup = function { var self = this; this.emptyContent; if ( config.xfd.type !== 'ffd' && config.xfd.type !== 'cfd' && !self.discussion.isBasicMode ) { this.rcatSelector = Dialog.RcatMultiselect(self.storeRcatData, self); }	this.inputData = new InputData(this.discussion); this.addToHeader(this.makeHeader(false, this.relisting)); this.addToBody(this.makePagesList); if ( this.relisting ) { this.addToBody(this.makeRationaleBox); } else { if ( !config.user.isSysop ) { this.addToBody(this.makeNonAdminNote); }		this.addToBody(this.makeCloseResult); this.addToBody(this.makeRationaleBox); if ( config.xfd.type === 'cfd' || this.discussion.isBasicMode ) { this.inputData.after = 'noAction'; } else { if ( config.xfd.type === 'afd' ) { this.rcatSelector.setValue(['']); }			this.addToBody(this.makeAfterActions); this.addToBody(this.makeOptions); }	}	this.addToFooter(this.makeButtons); this.addToFooter(this.makePreviewBox); this.resetHeight; this.display; };

// --- Set up for multimode close: --- Dialog.prototype.setupForMultiClose = function { var self = this; this.emptyContent; if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode ) { this.rcatSelector = Dialog.RcatMultiselect(self.storeRcatData, self); }	this.inputData = new InputData(this.discussion); this.inputData.initialiseMultimode; this.addToHeader(this.makeHeader(true)); if ( !config.user.isSysop ) { this.addToBody(this.makeNonAdminNote); }	this.addToBody(this.makePagesList(true)); this.addToBody(this.makeRationaleBox(true)); this.addToBody(this.makeAfterActions(true)); if ( config.xfd.type === 'afd' ) { this.rcatSelector.setValue(['']); }	this.addToBody(this.makeOptions(true)); this.addToFooter(this.makeButtons(true)); this.addToFooter(this.makePreviewBox); this.resetHeight; this.display; };

// Preview Dialog.prototype.showPreview = function(relist) {

var preview_wikitext = '';

if ( relist ) { preview_wikitext = ''; } else { preview_wikitext = "The result of the discussion was '''" + this.inputData.getResult+ "'" +		(( this.inputData.getTargetLink ) ? ' to ' + this.inputData.getTargetLink : ) + ( (this.inputData.getRationale) || '.' ) + ' ' +		config.user.sig; }

API.get({		action: 'parse',		contentmodel: 'wikitext',		text: preview_wikitext,		pst: '1'	}) .done(function(result) {		var preview_html = result.parse.text['*'];		$('#closeXFD-preview-output').html(preview_html).show.find('a').attr('target', '_blank');	}) .fail(function(code, jqxhr) {		$('#closeXFD-preview-output')		.empty		.append( $(' ')			.css('color', '#f00') .append(				'Preview failed. ',				formatApiError(code, jqxhr)			) )		.show;	}); };

// --- Evaluate if required form elements have been completed ---

// Show errors for anything required but not completed Dialog.prototype.showErrors = function(element_ids) { // remove any old error messages //$('.closeXFD-errorNote').parent.css('border', '').end.remove; $(element_ids.join(', ')) .filter(':visible') .addClass('closeXFD-errorNote') .fadeTo('fast', 0.33).fadeTo('fast', 0.8).fadeTo('fast', 0.4).fadeTo('fast', 1) .on('change.errorNote', function {		$(this)		.removeClass('closeXFD-errorNote')		.off('change.errorNote');	}); };

// Evaluate standard close Dialog.prototype.evaluateClose = function(preview) { var self = this; var errors = []; // Result if ( self.inputData.result ) { switch ( self.inputData.result ) { case 'custom': if ( !self.inputData.customResult ) { // Custom result not specified errors.push('#closeXFD-resultContainer-custom'); }				break; case 'retarget': case 'redirect': case 'soft redirect': case 'merge': if ( $('#closeXFD-result-target').val.trim === '' ) { // Target not specified errors.push('#closeXFD-resultContainer-target'); } else { if ( !self.inputData.target ) { // Invalid target window.alert('Bad ' + self.inputData.result +							' target: the title is invalid.'); errors.push('#closeXFD-resultContainer-target'); }				}				break; default: break; }	} else { // Result not selected errors.push('#closeXFD-resultOptions'); }	// Rationale var r = self.inputData.rationale; if ( r ) { // Prepend newline if it starts with a * or # r = r.replace(/^(\*|#)/, '\n$1'); // Append a newline if the last line starts with a * or # var n = r.lastIndexOf('\n'); if ( n > 0 && /(\*|#)/.test(r.slice(n+1,n+2) ) ) { r += '\n:'; }		self.inputData.rationale = r;	} // After actions & options if ( self.inputData.after ) { if ( self.inputData.after === 'holdingCell' && !self.inputData.holdcell ) { // Holding cell section not selected errors.push('#closeXFD-del-holdcell'); } else if (			config.xfd.type === 'tfd' &&			self.inputData.after === 'doMergeActions' &&			!self.inputData.mergeHoldcell		) { // Holding cell merge subsection not selected errors.push('#closeXFD-merge-holdcell'); }	} else { // After action not selected errors.push('#closeXFD-afterContainer'); }	if ( errors.length > 0 ) { // Show errors self.showErrors(errors); } else if ( preview ) { // Show preview self.showPreview; } else { // Start closing if ( 			( config.xfd.type === 'tfd' && self.inputData.after === 'doMergeActions' ) ||			( self.inputData.after === 'holdingCell' && self.inputData.holdcell !== 'ready' )		) { // Don't delete/tag talkpages when using holding cell (except for 'ready for deletion') self.inputData.dontdeletetalk = true; }		self.close; self.discussion.taskManager = new TaskManager(self.discussion); self.discussion.taskManager.start; } };

// Evaluate multimode close Dialog.prototype.evaluateMultimodeClose = function(preview) { var self = this; var errors = [];

// Check result summary specified if ( !self.inputData.result ) { errors.push('#closeXFD-resultSummary-input'); }

// Rationale var r = self.inputData.rationale; if ( r ) { // Prepend newline if it starts with a * or # r = r.replace(/^(\*|#)/, '\n$1'); // Append a newline if the last line starts with a * or # var n = r.lastIndexOf('\n'); if ( n > 0 && /(\*|#)/.test(r.slice(n+1,n+2) ) ) { r += '\n:'; }		self.inputData.rationale = r;	} // Check each page action / target for ( var p in self.inputData.pageActions ) { // Check action specified if ( !self.inputData.pageActions[p].action ) { errors.push('#'+self.inputData.pageActions[p].id); } else if ( 			self.inputData.pageActions[p] === 'redirect' ||			self.inputData.pageActions[p] === 'merge'		) { // Check target specified var targetInputId = self.inputData.pageActions[p].id.replace('action', 'tagert'); if ( $('#'+targetInputId).val.trim === '' ) { errors.push('#'+targetInputId); } else if ( !self.inputData.pageActions[p].target ) { window.alert('Bad ' + self.inputData.pageActions[p].action + 'target for ' +				p +' – the title is invalid.'); errors.push('#'+targetInputId); }		}	}

// Check holding cell selection (merge) if (		config.xfd.type === 'tfd' &&		self.inputData.inPageActions('merge') &&		!self.inputData.mergeHoldcell	) { errors.push('#closeXFD-merge-holdcell'); }	// Check holding cell selection (delete) if (		config.xfd.type === 'tfd' &&		self.inputData.inPageActions('del') &&		self.inputData.useHoldingCell &&		!self.inputData.holdcell	) { errors.push('#closeXFD-del-holdcell'); }	if ( errors.length > 0 ) { // Show errors self.showErrors(errors); } else if ( preview ) { // Show preview self.showPreview; } else { // Start closing if ( self.inputData.useHoldingCell && self.inputData.holdcell !== 'ready' ) { // Don't delete/tag talkpages when using holding cell (except for 'ready for deletion') self.inputData.dontdeletetalk = true; }		self.close; self.discussion.taskManager = new TaskManager(self.discussion); self.discussion.taskManager.start; }	};

/* ========== InputData class =================================================================== The raw data that the user entered in a dialog, and prototype get functions to obtain ready to use data -- */ // Constructor var InputData = function(discussion) { this.discussion = discussion; /* Defined by user interaction with dialog: this.speedy : {Boolean} this.customResult : {String} this.target : {Page object} this.result : {String} this.rationale : {String} this.resultPunct : {String} this.after : {String} this.deleteFirst : {Boolean} this.rcats : {String|Boolean false} this.useHoldingCell : {Boolean} this.holdcell : {String} this.mergeHoldcell : {String} this.dontdeletetalk : {Boolean} this.deleteredir : {Boolean} this.unlinkbackl : {Boolean} };

// -- InputData prototype --- */ // Setup for multimode close InputData.prototype.initialiseMultimode = function { this.multimode = true; this.pageActions = {}; };

// Check if an action has been specified for any of the pages (for multimode) InputData.prototype.inPageActions = function(action) { var self = this; if ( !self.multimode ) return false; for ( var p in self.pageActions ) { if ( self.pageActions[p].action === action ) return true; }	return false; };

// --- Get functions ---

// Get array of Page objects for a result (for multimode) InputData.prototype.getPages = function(result, result2, result3, result4) { var self = this; if ( !self.multimode ) return false; if ( result2 == null ) result2 = false; if ( result3 == null ) result3 = false; if ( result4 == null ) result4 = false; var output = []; for ( var p in self.pageActions ) { if (			self.pageActions[p].action === result ||			self.pageActions[p].action === result2 ||			self.pageActions[p].action === result3 ||			self.pageActions[p].action === result4		) { output.push(self.discussion.getPageByTitle(p)); }	}	return output; };

// Get the relist comment, escaping pipes which aren't within wikilinks or templates InputData.prototype.getRelistComment = function { return this.rationale.replace(/(\|)(?!(?:[^\[]*]|[^\{]*}))/g, '&#124;'); };

// Get the full result, for use as bolded result text or in edit summaries etc. InputData.prototype.getResult = function { if ( this.multimode ) { return this.result; } else { var output = ''; if ( this.speedy ) { output += 'speedy '; }		if (			this.deleteFirst &&			/(?:retarget|redirect|soft redirect)/.test(this.result)		) { output += 'delete and '; }		output += ( this.result === 'custom') ? this.customResult : this.result; return output; } };

// Get the page title of the target - for a particular page if in multimode InputData.prototype.getTarget = function(p) { if ( this.multimode && p ) { return this.pageActions[p].target.getPrefixedText; } else if ( /(?:retarget|redirect|soft redirect|merge)/.test(this.result) ) { return this.target && this.target.getPrefixedText; } else { return false; } };

// Multimode: Get an array of page titles of targets for a particular action ('redirect' or 'merge') // Single result: Return an array with one item, the target page title InputData.prototype.getTargetsArray = function(action) { var self = this; var output = []; if ( self.multimode ) { for ( var p in self.pageActions ) { if ( self.pageActions[p] && self.pageActions[p].action === action ) { output.push(self.pageActions[p].target.getPrefixedText); }		}	} else { output.push(self.target.getPrefixedText); }	return output; };

// Multimode: Get an array of Titles objects of targets, for either 'redirect' or 'merge' // Single result: Return an array with one item, the Title object for the target InputData.prototype.getTargetsObjectsArray = function { var self = this; var output = []; if ( self.multimode ) { for ( var p in self.pageActions ) { if ( self.pageActions[p] && /(redirect|merge)/.test(self.pageActions[p].action) ) { output.push(self.pageActions[p].target); }		}	} else { output.push(self.target); }	return output; };

// Get an array of page titles to be merged to a particlar target (for multimode) InputData.prototype.getPagesToBeMerged = function(target) { var self = this; var output = []; for ( var p in self.pageActions ) { if (			self.pageActions[p] &&			self.pageActions[p].action === 'merge' &&			self.pageActions[p].target.getPrefixedText === target		) { output.push(p); }	}	return output; };

// Get a link to the target - for a particular page if multimode - including fragment. // Is formatted as a wikilink, with preceding colon if needed, unless raw is true. InputData.prototype.getTargetLink = function(p, raw) { var targetObject = ( this.multimode && p ) ? this.pageActions[p].target : this.target; if ( !targetObject ) { return null; }

var targetFrag = ( targetObject.getFragment ) ? '#' + targetObject.getFragment : ''; var targetNS = targetObject.getNamespaceId; if ( raw ) { return targetObject.getPrefixedText + targetFrag; } else if ( targetNS === 6 || targetNS === 14 ) { return "" + targetObject.getPrefixedText + targetFrag + ""; } else { return "" + targetObject.getPrefixedText + targetFrag + ""; } };

// Gets the rational, preceded by a period (unless its not a new sentence) and a space InputData.prototype.getRationale = function { return ( this.rationale ) ? this.resultPunct + ' ' + this.rationale : ''; };

/* ========== TaskManager class ================================================================= Manages tasks - chooses which tasks to do, what data to pass to them, and keeps track of when (all) tasks are completed -- */ // Constructor var TaskManager = function(discussion, inputDataObject) { this.discussion = discussion; if ( inputDataObject != null ) { this.inputData = inputDataObject; } else { this.inputData = discussion.dialog.inputData; }	this.tasks = []; this.dfd = {}; };

// -- TaskManager prototype - */ // Sanity checks - confirm with user if there are a lot of pages or pages in unexpected namespaces TaskManager.prototype.makeSanityCheckWarnings = function { var self = this; var relisting = !self.inputData.result; var multimode = self.inputData.multimode; var warnings = [];

// Check if closing dicussion early (and it hasn't been relisted ) if ( !self.discussion.isOld && !self.discussion.isRelisted ) { warnings.push('It has not yet been 7 days since the discussion was listed.'); }	// Check for mass actions when closing: if ( !relisting ) { // Only if nominated pages will be edited if (			( relisting && /(ffd|tfd)/.test(config.xfd.type) ) ||			( !relisting && !multimode && self.inputData.after !== 'noAction' ) ||			( multimode && self.inputData.getPages('na').length !== self.discussion.pages.length )		) { if ( self.discussion.pages.length > 3 ) { warnings.push('Mass actions will be peformed (' + self.discussion.pages.length + ' nominated pages detected).'); }		}	}	// Check target page namespace: var targetObjectsArray = self.inputData.getTargetsObjectsArray; if ( targetObjectsArray ) { $.each(targetObjectsArray, function(_i, t) {			if ( t && !hasCorrectNamespace(t)) {				warnings.push( 'Target page "' + t.getPrefixedText + '" is not in the ' + config.mw.namespaces[(config.xfd.ns_number[0]).toString] + ' namespace.');			}		}); }	//Check namespaces of nominated pages var wrongNamespacePages = !self.discussion.isBasicMode && self.discussion.pages.filter(function(p) {		return !hasCorrectNamespace(p);	}); if ( wrongNamespacePages.length > 0 ) { warnings.push(			'The following pages are not in the ' +			config.mw.namespaces[(config.xfd.ns_number[0]).toString] + 			' namespace:<ul><li>' +			wrongNamespacePages.map(function(p){ return p.getPrefixedText; }).join('</li><li>') +			'</li>'		); }

if ( warnings.length === 0 ) { return false; } else { return ' ' + warnings.join(' ') + ' '; } };

// If nominated pages are redirects (at venues other than RfD), the script can't know if this was // appropriate, where results such as 'Delete' should be applied to the target, or an out-of-process // redirection, where results such as 'Delete' should be applied to the redirect /**ResolveRedirects * @returns {Promise<Boolean>} `true` when completed and okay to continue; rejected if aborted by user */ TaskManager.prototype.resolveRedirects = function { var self = this; // No need to process for RfD, or if basic closure, or no after actions var relisting = !self.inputData.result; var multimode = self.inputData.multimode; if (		// At RfD		config.xfd.type === 'rfd' ||		// Basic mode (no pages detected)		self.discussion.isBasicMode ||		// Relisting for other than FfD/TfD		( relisting && !/(ffd|tfd)/.test(config.xfd.type) ) ||		// Not multimode, no after actions		( !relisting && !multimode && self.inputData.after === 'noAction' ) ||		// Multimode, no action for every page		( multimode && self.inputData.getPages('na').length === self.discussion.pages.length )	) { // Nominate pages expected to be redirects, no need to resolve them return $.Deferred.resolve(true).promise; }	return API.get( {		action: 'query',		titles: self.discussion.getPageTitles.join('|'),		redirects: 1,		prop: 'info',		inprop: 'talkid'	} ) .then( function(result) {		if ( result.query && result.query.redirects ) {			var redirections = result.query.redirects.map(function(redirect) { return '<li>' + redirect.from + ' → ' + redirect.to + '</li>'; });

var redirects_msg = "The following nominated pages are redirects to other pages:<ul>" + redirections.join("") + "</ul>"; return multiButtonConfirm({				title: 'Use redirects or targets?',				message: redirects_msg,				actions: [					{ label: 'Cancel', flags: 'safe' },					{ label: 'Use redirects', action: 'reject' },					{ label: 'Use targets', action: 'accept', flags: 'progressive' }				],				size: 'medium'			}) .then(function(action) {				if ( action === 'accept' ) {					// Update discussion pages to use targets (for the ones that are redirects)					var apiResponsePages = arrayFromResponsePages(result);					self.discussion.pages = self.discussion.pages.map(function(page) { var redirection = result.query.redirects.find(function(redirect) {							return redirect.from === page.getPrefixedText;						}); if ( !redirection ) { return page; }						var target = apiResponsePages.find(function(p) {							return p.title === redirection.to;						}); var targetTitle = mw.Title.newFromText(target.title); setExistence(targetTitle, target.pageid > 0); setExistence(targetTitle.getTalkPage, target.talkid > 0); return targetTitle; });					return true;				} else if ( action === 'reject' ) {					// No need to update discussion's page data					return true;				} else {					// Abort closing, reset discussion links					self.discussion.showLinks;					return $.Deferred.reject('Cancelled by user');				}			});

} else { // No redirects present, just get on with it			return true; }	}); };

// --- Initialise --- // Set up for relist TaskManager.prototype.initialiseForRelist = function { var self = this;

self.tasks.push(new Task('getRelistInfo', self.discussion)); // Object to store gathered info self.relistInfo = {}; switch ( config.xfd.type ) { case 'afd': Array.prototype.push.apply(self.tasks, [				new Task('updateDiscussion', self.discussion),				new Task('updateOldLogPage', self.discussion),				new Task('updateNewLogPage', self.discussion)			]); // Deferred for when this relist is finished, keep track of it via its array index self.afdLogEditIndex = config.track.afdLogEdit.push($.Deferred) - 1; // Notify user self.discussion.setStatus('Waiting... (to avoid edit conflicts, previous relistings '+ 'need to be completed)'); break; case 'mfd': self.tasks.push(new Task('updateDiscussion', self.discussion)); break; case 'rfd': Array.prototype.push.apply(self.tasks, [				new Task('updateOldLogPage', self.discussion),				new Task('updateNewLogPage', self.discussion),				( !self.discussion.isBasicMode && self.discussion.pages.length > 1 )					? new Task('updateNomTemplates', self.discussion)					: ''			]			.filter(function(v){ return !!v; })			); break; default: // ffd, tfd Array.prototype.push.apply(self.tasks, [				new Task('updateOldLogPage', self.discussion),				new Task('updateNewLogPage', self.discussion),				( !self.discussion.isBasicMode ) ? new Task('updateNomTemplates', self.discussion) : ''			]			.filter(function(v){ return !!v; })			); break; }	self.initialiseFinally; };

TaskManager.prototype.initialiseForMultimodeClose = function { // Set up for multimode var self = this;

// Close discussion self.tasks.push(new Task('closeDisc', self.discussion)); // Add Old xfd templates if (		self.inputData.inPageActions('keep') ||		self.inputData.inPageActions('no consensus') ||		self.inputData.inPageActions('redirect') ||		( self.inputData.inPageActions('merge') && config.xfd.type !== 'tfd' ) ||		self.inputData.inPageActions('disambig')		) { self.tasks.push(new Task('addOldXfd', self.discussion, self.inputData.getPages(				'keep', 'no consensus', 'redirect', ( /(afd|mfd)/.test(config.xfd.type) ) ? 'merge' : 'disambig'			) ));	}	// For keep / no consensus results: if ( self.inputData.inPageActions('keep') || self.inputData.inPageActions('no consensus') ) { self.tasks.push(new Task('removeNomTemplates', self.discussion, self.inputData.getPages('keep', 'no consensus'))); }	// For redirect results: if ( self.inputData.inPageActions('redirect') ) { self.tasks.push(new Task('redirect', self.discussion, self.inputData.getPages('redirect'))); }	// For merge (not holding cell) results: if ( config.xfd.type !== 'tfd' && self.inputData.inPageActions('merge') ) { self.tasks.push(new Task('addMergeTemplates', self.discussion, self.inputData.getPages('merge'))); }	// For disambiguate results: if ( self.inputData.inPageActions('disambig') ) { self.tasks.push(new Task('disambiguate', self.discussion, self.inputData.getPages('disambig'))); }	// For delete (not holding cell) results: if ( self.inputData.inPageActions('del') && !self.inputData.useHoldingCell ) { Array.prototype.push.apply(self.tasks, [			new Task('peformDeletion', self.discussion, self.inputData.getPages('del')),			( self.inputData.dontdeletetalk ) ? null : new Task('deleteTalk', self.discussion, self.inputData.getPages('del')),			( self.inputData.deleteredir ) ? new Task('deleteRedirects', self.discussion, self.inputData.getPages('del')) : null,			( self.inputData.unlinkbackl ) ? new Task('unlinkBacklinks', self.discussion, self.inputData.getPages('del')) : null		]		.filter(function(v){ return !!v; })		); }	// For TfD holding cell results if ( config.xfd.type === 'tfd' ) { var doHcMerge = self.inputData.inPageActions('merge'); var doHcDelete = self.inputData.inPageActions('del') && self.inputData.useHoldingCell; if ( doHcMerge && doHcDelete ) { // Both 'merge' and 'delete' (via holding cell) results Array.prototype.push.apply(self.tasks, [				new Task('addBeingDeleted', self.discussion, self.inputData.getPages('merge', 'del')),				new Task('addToHoldingCell', self.discussion, self.inputData.getPages('merge', 'del')),				( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy', self.discussion, self.inputData.getPages('merge', 'del'))			]			.filter(function(v){ return !!v; })			); } else if ( doHcMerge ) { // Only 'merge' (via holding cell) results Array.prototype.push.apply(self.tasks, [				new Task('addBeingDeleted', self.discussion, self.inputData.getPages('merge')),				new Task('addToHoldingCell', self.discussion, self.inputData.getPages('merge'))			]			); } else if ( doHcDelete ) { // Only 'delete' (via holding cell) results Array.prototype.push.apply(self.tasks, [				new Task('addBeingDeleted', self.discussion, self.inputData.getPages('del')),				new Task('addToHoldingCell', self.discussion, self.inputData.getPages('del')),				( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy', self.discussion, self.inputData.getPages('del'))			]			.filter(function(v){ return !!v; })			); }	}	self.initialiseFinally; }; TaskManager.prototype.initialiseForClose = function { var self = this; // Set up for close self.tasks.push(new Task('closeDisc', self.discussion)); var addHoldcellTasks = function{ Array.prototype.push.apply(self.tasks, [			new Task('addBeingDeleted', self.discussion),			new Task('addToHoldingCell', self.discussion),			( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy', self.discussion)		]		.filter(function(v){ return !!v; })		); };	switch ( self.inputData.after ) { case 'doKeepActions': Array.prototype.push.apply(self.tasks, [				new Task('addOldXfd', self.discussion),				new Task('removeNomTemplates', self.discussion)			]); break; case 'doRedirectActions': Array.prototype.push.apply(self.tasks, [				new Task('addOldXfd', self.discussion),				new Task('redirect', self.discussion),				( self.inputData.result === 'soft redirect' ) ? null : new Task( 'removeCircularLinks', self.discussion)			]			.filter(function(v){ return !!v; })			); break; case 'doMergeActions': if ( config.xfd.type === 'tfd' ) { addHoldcellTasks; } else { Array.prototype.push.apply(self.tasks, [					new Task('addOldXfd', self.discussion),					new Task('addMergeTemplates', self.discussion)				]); }			break; case 'doDisambigActions': Array.prototype.push.apply(self.tasks, [				new Task('addOldXfd', self.discussion),				new Task('disambiguate', self.discussion)			]); break; case 'doDeleteActions': Array.prototype.push.apply(self.tasks, [				new Task('peformDeletion', self.discussion),				( self.inputData.dontdeletetalk ) ? null : new Task('deleteTalk', self.discussion),				( self.inputData.deleteredir ) ? new Task('deleteRedirects', self.discussion) : null,				( self.inputData.unlinkbackl ) ? new Task('unlinkBacklinks', self.discussion) : null			]			.filter(function(v){ return !!v; })			); break; case 'holdingCell': addHoldcellTasks; break; }	self.initialiseFinally; };

// After initialising for relist/close, set up deferred objects and start initial task TaskManager.prototype.initialiseFinally = function { var self = this; // Deferred objects self.dfd.initialTask = $.Deferred.done(function {		// When initial task is done, start others (if any)		if ( self.tasks.length > 1 ) {			$.each(self.tasks.slice(1), function(_i, t) { if ( t.start ) { t.start;	// TODO: set task status, errors, and warning here, from the returned promise } else { // TODO: Investigate what the point of this is. self.tasks[_i+1].start; }			});		}	});	if ( self.getTaskByName('unlinkBacklinks') ) { // When unlinkBacklinks query is completed, it is okay to delete redirects self.dfd.ublQuery = $.Deferred; } else if ( self.getTaskByName('deleteRedirects') ) { self.dfd.ublQuery = $.Deferred.resolve; }	// Start initial task if ( self.afdLogEditIndex ) { $.when( config.track.afdLogEdit[self.afdLogEditIndex-1] ) .then( function { 			self.discussion.setStatus('Relisting...');			self.tasks[0].start;			} ); } else { self.tasks[0].start; } };

TaskManager.prototype.start = function { var self = this;

var sanityCheckWarnings = self.makeSanityCheckWarnings; $.when(		( sanityCheckWarnings ) 		? multiButtonConfirm({ title: 'Warning', message: sanityCheckWarnings, actions: [ { label: 'Cancel', flags: 'safe' }, { label: 'Continue', action: 'proceed', flags: 'progressive' } ],			size: 'large' })		: $.Deferred.resolve('proceed')	) .then(function(action) {		if ( action !== 'proceed' ) {			// Reset discussion links			self.discussion.showLinks;			return;		}		// Resolve redirects...		return self.resolveRedirects;	}) .then(function(okayToProceed) {		if ( !okayToProceed ) {			return;		}		// ... then initialise		var isRelisting = !self.inputData.result;		if ( isRelisting ) {			self.initialiseForRelist;		} else if ( self.inputData.multimode ) {			self.initialiseForMultimodeClose;		} else {			self.initialiseForClose;		}	}); };

TaskManager.prototype.getTaskByName = function(name) { var self = this; for ( var i=0; i<self.tasks.length; i++ ) { if ( self.tasks[i].name === name ) { return self.tasks[i]; }	}	return false; };

TaskManager.prototype.makeTaskNote = function(task) { var $notice = $(' ') .addClass('xfdc-task-' + task.status) .addClass(task.name) .append(		$(' ').append(task.description),		': ',		$(' ').append(task.getStatusText),		$(' ').append(task.errors),		$(' ').append(task.warnings)	); return $notice; };

TaskManager.prototype.updateTaskNotices = function(task, skipFinishedCheck) { var self = this; var $notices = self.discussion.get$notices; if ( task && $notices.find('.'+task.name).length ) { // Update specified task var $taskNotice = self.discussion.get$notices.find('.'+task.name); $taskNotice.after(self.makeTaskNote(task)).remove; } else { // Update all tasks self.discussion.setNotices(			self.tasks.map(function(t) { return self.makeTaskNote(t); })		); }	var allFin = self.tasks.every(function(t) {			return t.isFinished;	}); // If every task is finished if ( !skipFinishedCheck && allFin ) { if ( self.afdLogEditIndex ) { config.track.afdLogEdit[self.afdLogEditIndex].resolve; }		self.discussion.setFinished; } };

TaskManager.prototype.abortTasks = function(reason) { var self = this; $.each(self.tasks, function(_i, t) {		t.status = 'aborted';	}); self.updateTaskNotices(null, true); if ( self.afdLogEditIndex ) { config.track.afdLogEdit[self.afdLogEditIndex].resolve; }	self.discussion.setFinished(reason); };

/* ========== Task class ========================================================================= Tasks represent the action or series of actions the script will peform for the closer. Each task object also has a description, status, and (if any) error or warning messages, which the TaskManager uses to display the task notice on the page. -- */ // Constructor var Task = function(taskname, discussion, pages) { this.discussion = discussion; this.inputData = discussion.taskManager.inputData; this.name = taskname; this.status = 'waiting'; this.errors = []; this.warnings = []; this.tracking = {}; if ( pages != null ) { this.pages = pages; } else { this.pages = null; }

var plural = ( this.pages ) ? this.pages.length > 1 : this.discussion.pages.length > 1; switch ( taskname ) { case 'closeDisc': this.description = 'Closing discussion'; break; case 'addOldXfd': this.description = 'Updating talk page' + (( plural ) ? 's' : ''); break; case 'removeNomTemplates': case 'addMergeTemplates': case 'disambiguate': this.description = 'Updating page' + (( plural ) ? 's' : ''); break; case 'peformDeletion': this.description = 'Deleting page' + (( plural ) ? 's' : ''); break; case 'addBeingDeleted': this.description = 'Updating template' + (( plural ) ? 's' : ''); break; case 'addToHoldingCell': this.description = 'Listing at holding cell'; break; case 'deleteTalk': this.description = 'Deleting talk page' + (( plural ) ? 's' : ''); break; case 'tagTalkWithSpeedy': this.description = 'Tagging talk page' + (( plural ) ? 's' : '') + ' for speedy deletion'; break; case 'deleteRedirects': this.description = 'Deleting redirects'; break; case 'unlinkBacklinks': this.description = 'Unlinking backlinks'; break; case 'redirect': if ( discussion.dialog.inputData.deleteFirst ) { this.description = 'Deleting page' + (( plural ) ? 's' : '') + ' and replacing with redirect' + (( plural ) ? 's' : ''); } else if ( config.xfd.type === 'rfd' ) { this.description = 'Retargeting redirect' + (( plural ) ? 's' : ''); } else { this.description = 'Making page' + (( plural ) ? 's' : '') + ' into redirect' + (( plural ) ? 's' : ''); }			break; case 'removeCircularLinks': this.description = 'Unlinking circular links on redirect target'; break; case 'getRelistInfo': this.description = 'Preparing to relist'; break; case 'updateDiscussion': this.description = 'Updating discussion'; break; case 'updateOldLogPage': this.description = 'Removing from old log page'; break; case 'updateNewLogPage': this.description = 'Adding to today\'s log page'; break; case 'updateNomTemplates': this.description = 'Updating link in nomination template' + (( plural ) ? 's' : ''); break; default: this.description = taskname; break; } };

// -- Task prototype */ // Notice-related functions Task.prototype.setDescription = function(d) { var self = this; this.description = d;	this.discussion.taskManager.updateTaskNotices(self); }; Task.prototype.setStatus = function(s) { var self = this; this.status = s;	this.discussion.taskManager.updateTaskNotices(self); }; Task.prototype.updateStatus = function { var self = this; this.discussion.taskManager.updateTaskNotices(self); }; Task.prototype.getStatusText = function { var self = this; switch ( self.status ) { // Not yet started: case 'waiting': return 'Waiting...'; // In progress: case 'started': var $msg = $(' ').append(					$(' ').attr({ 'src':'//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Ajax-loader%282%29.gif/'+ '40px-Ajax-loader%282%29.gif', 'width':'20', 'height':'5' })			);			if ( self.showTrackingProgress ) { var counts = this.tracking[self.showTrackingProgress]; $msg.append(					$(' ')					.css('font-size', '88%')					.append( ' (' +						(counts.success + counts.skipped) +						'&thinsp;/&thinsp;' +						counts.total +						')' )				);			}			return $msg; // Finished: case 'done': if (				self.discussion.taskManager.dfd.initialTask.state === 'pending' &&				self.name === self.discussion.taskManager.tasks[0].name			) { self.discussion.taskManager.dfd.initialTask.resolve; }			return 'Done!'; case 'aborted': case 'failed': case 'skipped': return extraJs.toSentenceCase(self.status) + '.'; default: // unknown return ''; } }; Task.prototype.isFinished = function { return $.inArray(this.status, ['done', 'aborted', 'failed', 'skipped']) !== -1; }; Task.prototype.addError = function(e, critical) { var self = this; this.errors.push($(' ').addClass('xfdc-notice-error').append(e)); if ( critical ) { this.status = 'failed'; }	this.discussion.taskManager.updateTaskNotices(self); }; Task.prototype.addWarning = function(w) { var self = this; this.warnings.push($(' ').addClass('xfdc-notice-warning').append(w)); this.discussion.taskManager.updateTaskNotices(self); }; Task.prototype.addApiError = function(code, jqxhr, explanation, critical) { var self = this; self.addError([		extraJs.makeErrorMsg(code, jqxhr),		' – ',		$(' ').append(explanation)	], !!critical); }; Task.prototype.setupTracking = function(key, total, allDoneCallback, allSkippedCallback) { var self = this; if ( allDoneCallback == null && allSkippedCallback == null ) { allDoneCallback = function { this.setStatus('done'); }; allSkippedCallback = function { this.setStatus('skipped'); }; }	this.tracking[key] = { success: 0, skipped: 0, total: total, dfd: $.Deferred .done($.proxy(allDoneCallback, self)) .fail($.proxy(allSkippedCallback, self)) }; }; Task.prototype.track = function(key, success, amount) { if ( success ) { this.tracking[key].success+=(amount||1); } else { this.tracking[key].skipped+=(amount||1); }

if ( key === this.showTrackingProgress ) { this.updateStatus; }

if ( this.tracking[key].skipped === this.tracking[key].total ) { this.tracking[key].dfd.reject; } else if ( this.tracking[key].success + this.tracking[key].skipped === this.tracking[key].total ) { this.tracking[key].dfd.resolve; } };

Task.prototype.start = function { return this.doTask[this.name](this); };

// Code to actually do the tasks Task.prototype.doTask = {}; // -- Closing tasks - // Close discussion Task.prototype.doTask.closeDisc = function(self) { // Notify task is started -- current (to-be-deprecated) method self.setStatus('started'); // Get nomination page content and remove {Closing} etc templates if present return API.get( {		action: 'query',		titles: self.discussion.nomPage,		prop: 'revisions',		rvprop: 'content|timestamp',		rvsection: self.discussion.sectionNumber,		indexpageids: 1	} ) .then( function(response) {		var id = response.query.pageids;		return response.query.pages[ id ].revisions[ 0 ];	} ) .then( function(revision) {		var contents = revision['*'];		var lastEditTime = revision.timestamp;		if ( contents.includes(config.xfd.wikitext.alreadyClosed) ) {			return $.Deferred.reject('aborted', null, 'Discussion already closed (reload page to see '+				'the actual close)');		}		/* Start-time check only possible for AFDs/MFDs. Other venues have multiple discussions on		 * the same page, which would give false-positives when a different section was edited.		 */		if ( config.xfd.type === 'afd' || config.xfd.type === 'mfd' ) {			var editedSinceScriptStarted = config.startTime < new Date(lastEditTime);			if ( editedSinceScriptStarted ) {				return $.Deferred.reject('aborted: edit conflict detected');			}		}

var section_heading = contents.slice(0, contents.indexOf('\n')); var decodeHtml = function(t) { return $(' ').html(t).text; };		var plain_section_heading = decodeHtml(	section_heading			.replace(/(?:^\s*=*\s*|\s*=*\s*$)/g, ) // remove heading markup			.replace(/\[\[\:?(?:[^\]]+\|)?([^\]]+)\]\]/g, '$1') // replace link markup with link text			.replace(/{{\s*[Tt]l[a-z]?\s*\|\s*([^}]+)}}/g, ) // replace tl templates			.replace(/s*}}/, '}}') // remove any extra spaces after replacing tl templates			.replace(/\s{2,}/g, ' ') // collapse multiple spaces into a single space			.trim		); var isCorrectSection = plain_section_heading === self.discussion.sectionHeader; if ( !isCorrectSection ) { return $.Deferred.reject('aborted: possible edit conflict (found section heading `' + plain_section_heading + '`)'); }

var xfd_close_top = config.xfd.wikitext.closeTop .replace(/__RESULT__/, self.inputData.getResult || '&thinsp;') .replace(/__TO_TARGET__/, ( self.inputData.getTarget ) ? ' to ' +				self.inputData.getTargetLink : '') .replace(/__RATIONALE__/, ( self.inputData.getRationale || '.')) .replace(/__SIG__/, config.user.sig); var section_contents = contents.slice(contents.indexOf('\n')+1) .replace(				/(|||\{\{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD\|.?\}\}| \[\[Category:Relisted AfD debates\|.*?\]\](\[\[Category:AfD debates relisted 3 or more times|.*?\]\])?<\/noinclude>)/gi,				''); var updated_top = ( config.xfd.type === 'afd' || config.xfd.type === 'mfd' ) ? xfd_close_top + '\n' + section_heading : section_heading + '\n' + xfd_close_top; var updated_section = updated_top + '\n' + section_contents.trim + '\n' + config.xfd.wikitext.closeBottom; return $.Deferred.resolve(updated_section, lastEditTime); } )	.then( function(updated_section, lastEditTime) { return API.postWithToken( 'csrf', {			action: 'edit',			title: self.discussion.nomPage,			section: self.discussion.sectionNumber,			text: updated_section,			summary: '/* ' + self.discussion.sectionHeader + ' */ Closed as ' +				self.inputData.getResult + config.script.advert,			basetimestamp: lastEditTime		} ); } )	.then( function { self.setStatus('done'); // Current (to-be-deprecated) method of setting status return 'done'; // Future method of setting status },		function(code, jqxhr, abortReason) { var message = [ 'Could not edit page ', extraJs.makeLink(self.discussion.nomPage), '; could not close discussion' ];			self.addApiError(code, jqxhr, message); var abortMessage = ( code.indexOf("aborted") === 0 && abortReason ) || ''; self.discussion.taskManager.abortTasks(abortMessage); return $.Deferred.reject(code, jqxhr, message); }	);

};

// Add old xfd template to talk pages Task.prototype.doTask.addOldXfd = function(self) {

// Notify task is started self.setStatus('started');

// Make wikitext of olf xfd template (non-AFD) var makeOldxfdWikitext = function(altpage) { var result = config.xfd.wikitext.oldXfd .replace(/__DATE__/, self.discussion.nomDate) .replace(/__SECTION__/, self.discussion.sectionHeader) .replace(/__RESULT__/, self.inputData.getResult) .replace(/__FIRSTDATE__/, self.discussion.firstDate) .replace(/__SUBPAGE__/, self.discussion.getNomSubpage); if ( altpage ) { result = result.replace('}}', ' |altpage='+altpage+'}}'); }		return result; };	// Add or update oldafdmulti template in section wikitext var makeNewWikitext = function(wikitext, pageTitle) { //Parts of this derived from https://en.wikipedia.org/wiki/User:Mr.Z-man/closeAFD2.js		var titleObject = mw.Title.newFromText(pageTitle); var PAGENAME = titleObject.getMain; var SUBJECTPAGENAME = config.monthNames[(titleObject.getNamespaceId-1).toString] + PAGENAME; var oldafdmulti = ''; var newtext; if ( oldAfdPatt.test(wikitext) ) { // Override the existing oldafdmulti newtext = wikitext.replace(oldAfdPatt, oldafdmulti+'\n'); } else { // Prepend to content (WP:Talk page layout is too complicated to automate) newtext = oldafdmulti+'\n'+wikitext; }		return newtext; };

// Get talk pages var talkTitles = self.discussion.getTalkTitles(self.pages); if ( talkTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('done'); return; }	// get talk page redirect status from api self.setupTracking('processed', talkTitles.length); // Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Check there's a corresponding nominated page var pageObj = self.discussion.getPageByTalkTitle(page.title); if ( !pageObj ) { return $.Deferred.reject("Unexpected title"); }		// Check corresponding page exists if ( !pageObj.exists ) { return $.Deferred.reject("Subject page does not exist"); }		var baseEditParams = { section: '0', summary: 'Old ' + config.xfd.type.toUpperCase + ' – ' + self.discussion.nomDate + ': ' + self.inputData.getResult +	config.script.advert, };		// Check redirect status if ( config.xfd.type !== 'afd' && page.redirect ) { // Is a redirect... if ( config.xfd.type === 'rfd' ) { // for RFD, ask what to do return OO.ui.confirm('"' + page.title + '" is currently a redirect. Okay ' +					'to replace with Old RFD template?') .then( function ( confirmed ) {					if (!confirmed) {						return $.Deferred.reject('skipped');					}					return $.extend(baseEditParams, { text: makeOldxfdWikitext, redirect: false });				} );			} else if ( config.xfd.type === 'mfd' ) { // For MFD, edit the redirect's target page, using the altpage parameter return $.extend(baseEditParams, {					prependtext: makeOldxfdWikitext(pageObj.getPrefixedText),					redirect: true				}); } else { // For other venues, append rather than prepend old xfd template return $.extend(baseEditParams, {					appendtext: '\n' + makeOldxfdWikitext,					redirect: false				}); }		} else { // Not a redirect. Attempt to find and consolidate existing banners var oldwikitext = page.missing ? '' : page.content; return $.extend(baseEditParams, {				text: makeNewWikitext(oldwikitext, page.title),				redirect: false,				}); }	};	API.editWithRetry(		talkTitles,		{rvsection: '0'},		transform,		function { self.track('processed', true); },		function(code, error, title) {			switch(code) {				case "Unexpected title":					self.addError([ 'API query result included unexpected talk page title ', extraJs.makeLink(title), '; this talk page will not be edited' ]);					self.track('processed', false);					break;				case "Subject page does not exist":					self.addError([ 'The subject page for ', extraJs.makeLink(title), ' does not exist; this talk page will not be edited' ]);					self.track('processed', false);					break;				case "skipped":					self.addWarning([ extraJs.makeLink(title), ' skipped' ]);					self.track('processed', true);					break;				default:					self.addApiError(code, error, [ 'Could not edit talk page ', extraJs.makeLink(title) ]);					self.track('processed', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of talk page' + ( talkTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

Task.prototype.doTask.removeNomTemplates = function(self, mergeWikitext) { // Notify task is started self.setStatus('started');

// Get page titles var pageTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true}); if ( pageTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('failed'); return; }	self.setupTracking(		'edit',		pageTitles.length,		( mergeWikitext ) ? function { this.track('alldone', true); } : null,		( mergeWikitext ) ? function { this.track('alldone', false); } : null	);

// Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Check there's a corresponding nominated page var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true}); if ( !pageObj ) { return $.Deferred.reject("unexpectedTitle"); }		// Check corresponding page exists if ( !pageObj.exists ) { return $.Deferred.reject("doesNotExist"); }		var oldWikitext = page.content; // Start building updated wikitext var updatedWikitext = ''; // For merging, unless the page is itself a merge target, prepend the mergeWikitext if (			mergeWikitext != null &&			$.inArray(page.title, self.inputData.getTargetsArray('merge')) === -1 		) { updatedWikitext = mergeWikitext[page.title]; }		if ( config.xfd.hasNomTemplate(oldWikitext) ) { try { // Remove nom template updatedWikitext += config.xfd.removeNomTemplate(oldWikitext); } catch(e){ // Error if multiple nom templates found return $.Deferred.reject("couldNotUpdate", e); }		} else { // Nomination template not found if ( updatedWikitext === '' ) { // Skip - nothing to change return $.Deferred.reject("nominationTemplateNotFound"); } else { // Show warning self.addWarning([					'Nomination template not found on page ',					extraJs.makeLink(page.title)				]); // Keep going - can still prepend wikitext for merging updatedWikitext += oldWikitext; }		}		return { text: updatedWikitext, summary: config.xfd.type.toUpperCase + ' closed as ' + self.inputData.getResult + config.script.advert };	};	API.editWithRetry(		pageTitles,		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "unexpectedTitle":					self.addError([ 'API query result included unexpected title ', extraJs.makeLink(title), '; this talk page will not be edited' ]);					self.track('edit', false);					break;				case "doesNotExist":					self.addError([ extraJs.makeLink(title), ' does not exist, and will not be edited' ]);					self.track('edit', false);					break;				case "nominationTemplateNotFound":					self.addWarning([ 'Nomination template not found on page ', extraJs.makeLink(title) ]);					self.track('edit', false);					break;				case "couldNotUpdate":					self.addError([ 'Could not update ', extraJs.makeLink(title), ': ',						error.message ]);					self.track('edit', false);					break;				default:					self.addApiError(code, error, [ 'Could not edit page ', extraJs.makeLink(title) ]);					self.track('edit', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of nominated page' + ( pageTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

Task.prototype.doTask.addMergeTemplates = function(self) {

// Notify task is started self.setStatus('started');

// Edit function var editTargetTalk = function(pageTitle, prependWikitext, isRetry) { API.postWithToken( 'csrf', {			action: 'edit',			title: pageTitle,			prependtext: prependWikitext,			summary: '' + self.discussion.getNomPageLink + ' closed as ' +				self.inputData.getResult + config.script.advert		} ) .done( function {			self.track('editTargetTalk', true);		}) .fail( function(code, jqxhr) {			if (!isRetry) {				return editTargetTalk(pageTitle, prependWikitext, true);			}			self.track('editTargetTalk', false);			self.addApiError(code, jqxhr, [ 'Could not edit page ', extraJs.makeLink(pageTitle) ]);		} );	};	// Get targets and their talk pages var targets = extraJs.uniqueArray(self.inputData.getTargetsArray('merge')); self.setupTracking('alldone', 2); self.setupTracking(		'editTargetTalk',		targets.length,		function { this.track('alldone', true); },		function { this.track('alldone', false); }	); // Strings for merge templates var debate = self.discussion.getNomSubpage; var today = new Date; var curdate = today.getUTCDate.toString + ' ' + config.monthNames[today.getUTCMonth] + ' ' + today.getUTCFullYear.toString; // Object to hold the 'merge to' template for each page var mergetoWikitext = {}; // For each target... for ( var i=0; i<targets.length; i++ ) { // Pages to be merged to target var mergeTitles; if ( self.inputData.multimode ) { mergeTitles = self.inputData.getPagesToBeMerged(targets[i]); } else { mergeTitles = self.discussion.getPageTitles(self.pages); }

// Make 'merge to' template for pages to be merged var mergetoTemplate = config.xfd.wikitext.mergeTo .replace(/__TARGET__/, targets[i]) .replace(/__DEBATE__/, debate) .replace(/__DATE__/, curdate) .replace(/__TARGETTALK__/, mw.Title.newFromText(targets[i]).getTalkPage.getPrefixedText);

// Make 'merge from' template for the target's talk page var mergefromTemplates = []; for (var ii=0; ii<mergeTitles.length; ii++) { mergefromTemplates.push(				config.xfd.wikitext.mergeFrom				.replace(/__NOMINATED__/, mergeTitles[ii])				.replace(/__DEBATE__/, debate)				.replace(/__DATE__/, curdate)			); // Add 'merge to' template to holding object mergetoWikitext[mergeTitles[ii]] = mergetoTemplate; }		// Check if the target is one of the nominated pages if ( $.inArray(targets[i], self.discussion.getPageTitles(self.pages)) !== -1 ) { // No need to add 'merge from' template - this was a nominated page, and will have an // old xfd template put on its talkpage specify the merge result. self.track('editTargetTalk', false); continue; } else { // Edit target talkpage editTargetTalk(mw.Title.newFromText(targets[i]).getTalkPage.getPrefixedText, mergefromTemplates.join('')); }	}	// Replace nomination templates with 'merge to' templates self.doTask.removeNomTemplates(self, mergetoWikitext); };

Task.prototype.doTask.disambiguate = function(self) { // Notify task is started self.setStatus('started');

var pageTitles = self.discussion.getPageTitles(self.pages); if ( pageTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('failed'); return; }	self.setupTracking('edit', pageTitles.length); // Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Check there's a corresponding nominated page var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true}); if ( !pageObj ) { return $.Deferred.reject("unexpectedTitle"); }		// Check corresponding page exists if ( !pageObj.exists ) { return $.Deferred.reject("doesNotExist"); }		var oldWikitext = page.content; var updatedWikitext = '';

if ( config.xfd.regex.fullNomTemplate.test(oldWikitext) ) { updatedWikitext = oldWikitext.replace(config.xfd.regex.fullNomTemplate, '').trim; } else { self.addWarning([				'Nomination template not found on page ',				extraJs.makeLink(page.title)			]); updatedWikitext = oldWikitext.replace(/^#REDIRECT/mi, '*'); }		if ( !/(?:disambiguation|disambig|dab|Mil-unit-dis|Numberdis)[^{]*}}/i.test(updatedWikitext) ) { updatedWikitext += '\n'; updatedWikitext.trim; }		return { text: updatedWikitext, summary: config.xfd.type.toUpperCase + ' closed as ' + self.inputData.getResult + config.script.advert };	};	API.editWithRetry(		pageTitles,		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "unexpectedTitle":					self.addError([ 'API query result included unexpected title ', extraJs.makeLink(title), '; this talk page will not be edited' ]);					self.track('edit', false);					break;				case "doesNotExist":					self.addError([ extraJs.makeLink(title), ' does not exist, and will not be edited' ]);					self.track('edit', false);					break;				default:					self.addApiError(code, error, [ 'Could not edit page ', extraJs.makeLink(title) ]);					self.track('edit', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of nominated page' + ( pageTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

Task.prototype.doTask.peformDeletion = function(self) {

// Notify task is started self.setStatus('started'); // Delete with the api var apiDeletePage = function(pageTitle, isRetry) { API.postWithToken( 'csrf', {			action: 'delete',			title: pageTitle,			reason:  + self.discussion.getNomPageLink +  + config.script.advert		} ) .done( function {			self.track('del', true);		} ) .fail( function(code, jqxhr) {			if (!isRetry) {				return apiDeletePage(pageTitle, true);			}			self.track('del', false);			self.addApiError(code, jqxhr, [ 'Could not delete page ', extraJs.makeLink(pageTitle) ]);		} );	};	var pages = self.pages || self.discussion.pages; self.setupTracking('del', pages.length); // For each page, check it exists, then delete with api or add warning for ( var i=0; i<pages.length; i++ ) { if ( pages[i].exists ) { apiDeletePage(pages[i].getPrefixedText); } else { self.addWarning([				extraJs.makeLink(pages[i].getPrefixedText),				' skipped: does not exist (may have already been deleted by others)'			]); self.track('del', false); }	} };

Task.prototype.doTask.addBeingDeleted = function(self) {

// Notify task is started self.setStatus('started');

// Merge targets and pages to be merged (if any) var mergeTargets = []; var mergeTitles = []; if ( self.inputData.inPageActions('merge') || self.inputData.result === 'merge' ) { mergeTargets = self.inputData.getTargetsArray('merge'); mergeTitles = self.discussion.getPageTitles( 			(self.inputData.multimode ) ? self.inputData.getPages('merge') : self.pages		); }

var templateTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true});

self.setupTracking('edit', templateTitles.length);

// Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Check there's a corresponding nominated page var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true}); if ( !pageObj ) { return $.Deferred.reject("unexpectedTitle"); }		// Check corresponding page exists if ( !pageObj.exists ) { return $.Deferred.reject("doesNotExist"); }		// Replace {Template for discussion/dated} or {Tfm/dated} (which		// may or may not be noincluded) var isModule = ( page.title.indexOf('Module:') === 0 ); var inclusionTag = ( isModule ) ? 'includeonly' : 'noinclude'; var oldWikitext = page.content; var updatedWikitext = ''; var editsum = ''; try { if ( $.inArray(page.title, mergeTargets) !== -1 ) { // If this is a merge target, don't add anything - just remove nom template updatedWikitext = config.xfd.removeNomTemplate(oldWikitext); editsum = '' + self.discussion.getNomPageLink + ' closed as ' + self.inputData.getResult + config.script.advert; } else if ( self.inputData.holdcell === 'ready' ) { // If holding cell section is 'ready for deletion', tag for speedy deletion updatedWikitext = '<' + inclusionTag + '></' + inclusionTag + '>' + config.xfd.removeNomTemplate(oldWikitext); editsum = 'G6 Speedy deletion nomination, per ' +					self.discussion.getNomPageLink + '' + config.script.advert; } else { // Add Being deleted template, remove nom template updatedWikitext = '<' + inclusionTag + '></' + inclusionTag + '>' + config.xfd.removeNomTemplate(oldWikitext); editsum = 'Per ' + self.discussion.getNomPageLink + ', '+ 'added ' + config.script.advert; }		} catch(e){ // Error if multiple nom templates found return $.Deferred.reject("couldNotUpdate", e); }		return { text: updatedWikitext, summary: editsum };	};

API.editWithRetry(		templateTitles,		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "unexpectedTitle":					self.addError([ 'API query result included unexpected title ', extraJs.makeLink(title), '; this talk page will not be edited' ]);					self.track('edit', false);					break;				case "doesNotExist":					self.addError([ extraJs.makeLink(title), ' does not exist, and will not be edited' ]);					self.track('edit', false);					break;				case "couldNotUpdate":					self.addError([ 'Could not update ', extraJs.makeLink(title), ': ',						error.message ]);					self.track('edit', false);					break;				default:					self.addApiError(code, error, [ 'Could not edit page ', extraJs.makeLink(title) ]);					self.track('edit', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of nominated page' + ( templateTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

Task.prototype.doTask.addToHoldingCell = function(self) {

// Notify task is started self.setStatus('started'); var holdCellSections = [ self.inputData.holdcell || null, self.inputData.mergeHoldcell || null ]	.filter(function(v){ return !!v; }) .map(function(v){ return config.xfd.holdingCellSectionNumber[v]; }); self.setupTracking('edit', 1);

// Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Get page contents, split into section var sectionsArray = page.content.split(/\n={3,}/).map(function(section) {			var headingSigns = /^[^=]+(=+)\n/.exec(section);			if (!headingSigns) {				return section;			}			return headingSigns[1] + section;		}); holdCellSections.forEach(function(holdCellSection) {			var isMergeSection = config.xfd.holdingCellSectionNumber["merge-infobox"] <= holdCellSection &&				holdCellSection <= config.xfd.holdingCellSectionNumber["merge-meta"];			var listingPages;			if ( !self.inputData.multimode ) {				listingPages = self.discussion.pages;			} else if ( isMergeSection ) {				listingPages = self.inputData.getPages('merge');			} else {				listingPages = self.inputData.getPages('del');			}			var tfdlTemplates = '';			listingPages.forEach(function(listingPage) { //Check namespace and existance if ( !hasCorrectNamespace(listingPage) ) { self.addError([						extraJs.makeLink(listingPage),						' is not in the ' + config.mw.namespaces[config.xfd.ns_number[0].toString] +						' namespace, and will not be listed at the holding cell'					]); } else if ( !listingPage.exists ) { self.addError([						extraJs.makeLink(listingPage),						' does not exist, and will not be listed at the holding cell'					]); } else { tfdlTemplates += '*\n'; }			});			if ( tfdlTemplates ===  ) {				// If all don't exist or are in wrong namespace, then there's nothing to do				self.track('edit', false);				return;			}			// Make new section wikitext			sectionsArray[holdCellSection] = sectionsArray[holdCellSection]				.replace(/\n*^\*\s*None currently\s*$(?![^<]*?-->)/gim, ) // Remove "* None currently" except if inside a 				.trim + '\n' + tfdlTemplates;		}); var updatedContent = sectionsArray.join("\n"); if (updatedContent === page.content) { return $.Deferred.reject("noChangesMade"); }		return { text: updatedContent, summary: 'Listing template(s) per ' + self.discussion.getNomPageLink + '' + config.script.advert };	};	API.editWithRetry(		[config.xfd.subpagePath + 'Holding cell'],		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "noChangesMade":					self.addError([ 'Did not find any changes to make to the holding cell' ]);					self.track('edit', false);					break;				default:					self.addApiError(code, error, [ 'Could not add templates to holding cell' ]);					self.track('edit', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of holding cell');			self.setStatus('failed');		} // other errors handled above	} ); };

Task.prototype.doTask.deleteTalk = function(self) { // Notify task is started self.setStatus('started');

// Get talk pages var talkTitles = self.discussion.getTalkTitles(self.pages); if ( talkTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('skipped'); return; }	self.setupTracking('del', talkTitles.length);

// Delete with the api var apiDeleteTalkPage = function(talkTitle, isRetry) { API.postWithToken( 'csrf', {			action: 'delete',			title: talkTitle,			reason: 'G8: Talk page of deleted page.' + config.script.advert		} ) .done( function {			self.track('del', true);		} ) .fail( function(code, jqxhr) {			if (!isRetry) {				return apiDeleteTalkPage(talkTitle, true);			}			self.track('del', false);			self.addApiError(code, jqxhr, [ 'Could not delete page ', extraJs.makeLink(talkTitle) ]);		} );	};	//For each talk page, check that it exists and is eligible for a G8 speedy, then delete it	talkTitles.forEach( function(talkTitle) {		var subjectPage = self.discussion.getPageByTalkTitle(talkTitle);		var isUserTalkBasePage = ( subjectPage.getNamespaceId === 2 ) && ( !talkTitle.includes('/') );

if ( !subjectPage.getTalkPage.exists ) { self.addWarning([				extraJs.makeLink(talkTitle),				' skipped: does not exist (may have already been deleted by others)'			]); self.track('del', false); return; }		if ( isUserTalkBasePage ) { self.addWarning([				extraJs.makeLink(talkTitle),				' skipped: base user talk page (not eligible for G8 speedy deletion)'			]); self.track('del', false); return; }

// Delete with the api apiDeleteTalkPage(talkTitle); }); };

Task.prototype.doTask.tagTalkWithSpeedy = function(self) {

// Notify task is started self.setStatus('started'); // Get talk pages var talkTitles = self.discussion.getTalkTitles(self.pages); if ( talkTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('skipped'); return; }	// get talk page redirect status from api self.setupTracking('tag', talkTitles.length);

var apiTagPage = function(pageTitle, isRetry) { API.postWithToken( 'csrf', {			action: 'edit',			title: pageTitle,			prependtext: '\n',			summary: 'G8 Speedy deletion nomination, per ' +				self.discussion.getNomPageLink + '' + config.script.advert,			nocreate: 1		} ) .done( function {			self.track('tag', true);		} ) .fail( function(code, jqxhr) {			if (code === "missingtitle") {				self.track('tag', false);				self.addWarning([ extraJs.makeLink(pageTitle), ' skipped: does not exist (may have already been deleted by others)' ]);				return;			} else if (!isRetry) {				return apiTagPage(pageTitle, true);			}			self.track('tag', false);			self.addApiError(code, jqxhr, [ 'Could not tag page ', extraJs.makeLink(pageTitle), ' for speedy deletion' ]);		} );	};	//For each talk page, check that it exists, then tag it	talkTitles.forEach(function(talkTitle) {		var subjectPage = self.discussion.getPageByTalkTitle(talkTitle);		if ( !subjectPage.getTalkPage.exists ) {			self.addWarning([ extraJs.makeLink(talkTitle), ' skipped: does not exist (may have already been deleted by others)' ]);			self.track('tag', false);			return;		}		apiTagPage(talkTitle);	}); };

Task.prototype.doTask.deleteRedirects = function(self) { // If unlinking backlinks, wait until that task has the info it needs $.when(self.discussion.taskManager.dfd.ublQuery).then(function{		// Notify task is started		self.setStatus('started');		self.setupTracking('alldone', 2);		var redirectTitles = [];		var redirectTalkPageIds = [];		// Delete redirect with the api		var apiDeleteRedir = function(pageTitle, isRetry) {			API.postWithToken( 'csrf', { action: 'delete', title: pageTitle, reason: 'Delete redirect: ' + self.discussion.getNomPageLink + ' closed as ' + self.inputData.getResult + config.script.advert } )			.done( function { self.track('delRedir', true); } )			.fail( function(code, jqxhr) { if (!isRetry) { return apiDeleteRedir(pageTitle, true); }				self.track('delRedir', false); self.addApiError(code, jqxhr, [					'Could not delete redirect ',					extraJs.makeLink(pageTitle)				]); } );		};

// Delete redirect talkpage with the api var apiDeleteRedirTalk = function(talkpageid, isRetry) { API.postWithToken( 'csrf', {				action: 'delete',				pageid: talkpageid,				reason: 'G8: Talk page of deleted page.' + config.script.advert			} ) .done( function {				self.track('delRedirTalk', true);			} ) .fail( function(code, jqxhr) {				if (!isRetry) {					return apiDeleteRedirTalk(talkpageid, true);				}				self.track('delRedirTalk', false);				self.addApiError(code, jqxhr, [ 'Could not delete ', $('<a>').attr({						'href':'https://en.wikipedia.org/wiki/?curid='+talkpageid,						'target':'_blank'					}).text('redirect talk page <'+talkpageid+'>') ]);			} );		};		// If okay to delete redirects var doDeleteRedirects = function { // Set tracking self.setupTracking(				'delRedir',				redirectTitles.length,				function { this.track('alldone', true); },				function { this.track('alldone', false); }			); // Delete each redirect redirectTitles.forEach(function(redirectTitle) {				apiDeleteRedir(redirectTitle);			}); // Check if talk pages were found if ( redirectTalkPageIds.length === 0 ) { self.track('alldone', true); return; }			// Set tracking self.setupTracking(				'delRedirTalk',				redirectTalkPageIds.length,				function { this.track('alldone', true); },				function { this.track('alldone', false); }			); // Delete each talk page redirectTalkPageIds.forEach(function(redirectTalkId) {				apiDeleteRedirTalk(redirectTalkId);			}); };		var processRedirects = function(result) { if ( !result.query || !result.query.pages ) { // No results self.addWarning('none found'); self.setStatus('skipped'); return; }			// Gather redirect titles into array $.each(result.query.pages, function(_i, info) {				redirectTitles.push(info.title);				if ( info.talkid ) { redirectTalkPageIds.push(info.talkid); }			}); // Continue query if needed if ( result.continue ) { apiQuery($.extend(query, result.continue)); return; }			// Check if redirects were found if ( redirectTitles.length === 0 ) { self.addWarning('none found'); self.setStatus('skipped'); return; }			// Warn if there will be mass action, allow user to cancel if ( redirectTitles.length >= 10 ) {

var processAction = function(action) { if ( action === 'accept' ) { doDeleteRedirects; } else if ( action === 'show' ) { multiButtonConfirm({							title: 'Warning',							message: 'Mass action to be peformed: delete '+ redirectTitles.length +							' redirects:<ul><li>' + redirectTitles.join('</li><li>') + '</li></ul>',							actions: [								{ label:'Cancel', flags:'safe' },								{ label:'Delete redirects', action:'accept', flags:'progressive' }							],							size: 'medium'						}) .then(processAction); } else { self.addWarning('Cancelled by user'); self.setStatus('skipped'); }				};

multiButtonConfirm({					title: 'Warning',					message: 'Mass action to be peformed: delete '+ redirectTitles.length + ' redirects.',					actions: [						{ label:'Cancel', flags:'safe' },						{ label:'View redirects...', action:'show' },						{ label:'Delete redirects', action:'accept', flags:'progressive' }					],					size: 'medium'				}) .then(processAction); } else { doDeleteRedirects; }		};		// Get redirects var query = { action: 'query', titles: self.discussion.getPageTitles(self.pages).join('|'), generator: 'redirects', grdlimit: 500, prop: 'info', inprop: 'talkid' };		var apiQuery = function(q) { API.get( q ) .done( processRedirects ) .fail( function(code, jqxhr) {				self.addApiError(code, jqxhr, 'Could not obtain redirects');				self.setStatus('failed');			} ); };		apiQuery(query); }); };

Task.prototype.doTask.unlinkBacklinks = function(self) {

// Notify task is started self.setStatus('started'); var pageTitles = self.discussion.getPageTitles(self.pages); var redirectTitles = []; // Ignore the following titles, and any of their subpages var ignoreTitleBases = [ 'Template:WPUnited States Article alerts', 'Template:Article alerts columns', 'Template:Did you know nominations' ];	var getBase = function(title) { return title.split('/')[0]; };	var blresults = []; var iuresults = []; //convert results (arrays of objects) to titles (arrays of strings), removing duplicates var flattenToTitles = function(results) { return results.reduce(			function(flatTitles, result) {				if ( result.redirlinks ) {					if ( !redirectTitles.includes(result.title)) {						redirectTitles.push(result.title);					}					return flatTitles.concat( result.redirlinks.reduce(							function(flatRedirLinks, redirLink) {								if ( flatTitles.includes(redirLink.title) || pageTitles.includes(redirLink.title) || ignoreTitleBases.includes(getBase(redirLink.title)) ) {									return flatRedirLinks;								} else {									return flatRedirLinks.concat(redirLink.title);								}							},							[]						) );				} else if ( result.redirect === '' || flatTitles.includes(result.title) || pageTitles.includes(result.title) || ignoreTitleBases.includes(getBase(result.title)) ) {					return flatTitles;				} else {					return flatTitles.concat(result.title);				}			},			[]		); };

/**	 * @param {String} pageTitle * @param {String} wikitext * @returns {Promise(String)} updated wikitext, with any list items either removed or unlinked */	var checkListItems = function(pageTitle, wikitext, isMajorEdit) { // Find lines marked with, and the preceding section heading (if any) var toReview = /^(.*)$/m.exec(wikitext); if ( !toReview ) { // None found, no changes needed return $.Deferred.resolve(wikitext, !!isMajorEdit).promise; }		// Find the preceding heading, if any var precendingText = wikitext.split('')[0]; var allHeadings = precendingText.match(/^=+.+?=+$/gm); var heading = ( !allHeadings ) ? null : allHeadings[allHeadings.length - 1] .replace(/(^=* *| *=*$)/g, '') .replace(/\[\[([^\|\]]*?)\|([^\]]*?)\]\]/, '$2') .replace(/\[\[([^\|\]]*?)\]\]/, '$1'); // Prompt user return multiButtonConfirm({			title: 'Review unlinked list item',			message: ' A backlink has been removed from the following list item: ' +				' List: ' + pageTitle + '#' + heading + '' : ']]'				) +				' ' + toReview[1] + ' ' +				' Please check if the item matches the list\'s selection criteria'+				' before deciding to keep or remove the item from the list. ',			actions: [				{ label:'Keep item', action:'keep', icon:'articleCheck', flags:'progressive' },				{ label:'Keep and request citation', action:'keep-cite', icon:'flag' },				{ label:'Remove item', action:'remove', icon:'trash', flags:'destructive'}			],			size: 'large'		}) .then(function(action) {			if ( action === 'keep' ) {				// Remove the void from the start of the line				wikitext = wikitext.replace(/^/m, );			} else if ( action === 'keep-cite' ) {				// Remove the void from the start of the line, add citation needed at the end				wikitext = wikitext.replace(/^(.*)(\n?)/m, '$1$2');			} else {				// Remove the whole line, mark as a major edit				wikitext = wikitext.replace(/^.*\n?/m, );				isMajorEdit = true;			}			// Iterate, in case there is more to be reviewed			return checkListItems(pageTitle, wikitext, isMajorEdit);		}); };	// Function to transform a simplified API page object into edit parameters for API.editWithRetry var confirmationPromises = [$.Deferred]; // Each transformation must wait for the previous one to finish dipslaying confirmation prompts var transform = function(page) { var previousPromise = confirmationPromises[confirmationPromises.length-1]; var thisConfirmationPromise = $.Deferred; confirmationPromises.push(thisConfirmationPromise); return previousPromise.then(function{			var oldWikitext = page.content;			var newWikitext = extraJs.unlink( oldWikitext, pageTitles.concat(redirectTitles), page.ns, !!page.categories );			if (oldWikitext === newWikitext) {				thisConfirmationPromise.resolve;				return $.Deferred.reject('skippedNoLinks');			}			var checkListItemsPromise = checkListItems(page.title, newWikitext);			checkListItemsPromise.then(thisConfirmationPromise.resolve);			return checkListItemsPromise.then(function(updatedWikitext, isMajorEdit) { var req = { text: updatedWikitext, summary: 'Removing link(s)' + ( isMajorEdit ? ' / list item(s)' : '') + (( config.xfd.type === 'ffd' ) ? ' / file usage(s)' : '' ) + ': ' + self.discussion.getNomPageLink + ' closed as ' + self.inputData.getResult + config.script.advert, nocreate: 1 };				if ( !isMajorEdit ) { req.minor = 1; }				return req; });		});	};	var processResults = function { // Flatten results arrays if ( blresults.length !== 0 ) { blresults = flattenToTitles(blresults); }		if ( iuresults.length !== 0 ) { iuresults = flattenToTitles(iuresults); // Remove image usage titles that are also in backlikns results iuresults = iuresults.filter(function(t) { return $.inArray(t, blresults) === -1; }); }

// Check if, after flattening, there are still backlinks or image uses if ( blresults.length === 0 && iuresults.length === 0 ) { self.addWarning('none found'); self.setStatus('skipped'); return; }

// Ask user for confirmation var heading = 'Unlink backlinks'; if ( iuresults.length !== 0 ) { heading += '('; 			if ( blresults.length !== 0 ) {				heading += 'and ';			}			heading += 'file usage)'; }		heading += ':'; var para = ' All '+ (blresults.length + iuresults.length) + ' pages listed below may be '+ 'edited (unless backlinks are only present due to transclusion of a template). '+			' To process only some of these pages, use Twinkle\'s unlink tool instead. '+			' Use with caution, after reviewing the pages listed below. '+			'Note that the use of high speed, high volume editing software (such as this tool and '+			'Twinkle\'s unlink tool) is subject to the Bot policy\'s Assisted editing guidelines '+ ' ';		var list = '<ul>'; if ( blresults.length !== 0 ) { list += '<li>' + blresults.join('</li><li>') + '</li>'; }		if ( iuresults.length !== 0 ) { list += '<li>' + iuresults.join('</li><li>') + '</li>'; }		list += '<ul>'; multiButtonConfirm({			title: heading,			message: para + list,			actions: [				{ label: 'Cancel', flags: 'safe' },				{ label: 'Remove backlinks', action: 'accept', flags: 'progressive' }			],			size: 'medium'		}) .then(function(action) {			if ( action ) {				var unlinkTitles = iuresults.concat(blresults);				self.setupTracking('unlink', unlinkTitles.length);				self.showTrackingProgress = 'unlink';				// Define api callbacks outside of loop				var onSuccess = function { self.track('unlink', true); };				var onFailure = function(code, error, title) {					switch(code) {						case "skippedNoLinks":							self.addWarning(['Skipped ', extraJs.makeLink(title), ' (no direct links)' ]);							self.track('unlink', false);							break;						default:							self.addApiError(code, error, [ 'Could not remove backlinks from ', extraJs.makeLink(title) ]);							self.track('unlink', false);					}				};				var onReadFail = function(errortype, code, jqxhr) {					if ( errortype === "read" ) {						self.addApiError(code, jqxhr, 'Could not read contents of pages; could not remove backlinks');						self.setStatus('failed');					} // other errors handled above				};				// loop over unlinkTitles in lots of 50 (max for Api)				for (var ii=0; ii<unlinkTitles.length; ii+=50) {					API.editWithRetry( unlinkTitles.slice(ii, ii+49).join('|'), {							prop: 'categories|revisions', clcategories: 'Category:All disambiguation pages', },						transform, onSuccess, onFailure ).fail( onReadFail );				}				confirmationPromises[0].resolve;			} else {				self.addWarning('Cancelled by user');				self.setStatus('skipped');			}		}); };

// Queries var blParams = { list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 'max', blnamespace: config.xfd.ns_unlink, blredirect: 1 };	var iuParams = { list: 'backlinks|imageusage', iutitle: '', iufilterredir: 'nonredirects', iulimit: 'max', iunamespace: config.xfd.ns_unlink, iuredirect: 1 };	var query = pageTitles.map(function(page) {		return $.extend( { action: 'query' }, blParams, { bltitle: page }, ( config.xfd.type === 'ffd' ) ? iuParams : null, ( config.xfd.type === 'ffd' ) ? { iutitle: page } : null );	});	// Variable for incrementing current query var qIndex = 0; // Function to do Api query var apiQuery = function(q) { API.get( q ) .done( processBacklinks ) .fail( function(code, jqxhr) {			self.addApiError(code, jqxhr, 'Could not retrieve backlinks');			self.setStatus('failed');			// Allow delete redirects task to begin			self.discussion.taskManager.dfd.ublQuery.resolve;		} ); };	// Process api callbacks var processBacklinks = function(result) { // Gather backlink results into array if ( result.query.backlinks ) { blresults = blresults.concat(result.query.backlinks); }		// Gather image usage results into array if ( result.query.imageusage ) { iuresults = iuresults.concat(result.query.imageusage); }		// Continue current query if needed if ( result.continue ) { apiQuery($.extend({}, query[qIndex], result.continue)); return; }		// Start next query, unless this is the final query qIndex++; if ( qIndex < query.length ) { apiQuery(query[qIndex]); return; }		// Allow delete redirects task to begin self.discussion.taskManager.dfd.ublQuery.resolve; // Check if any backlinks or image uses were found if ( blresults.length === 0 && iuresults.length === 0 ) { self.addWarning('none found'); self.setStatus('skipped'); return; }		// Process the results processResults; };	// Get started apiQuery(query[qIndex]); };

Task.prototype.doTask.redirect = function(self) {

// Notify task is started self.setStatus('started'); var pageTitles = self.discussion.getPageTitles(self.pages); var deleteFirst = self.inputData.deleteFirst; var softRedirect = self.inputData.result === 'soft redirect'; var rcats = self.inputData.rcats; var rcatshell = ( rcats ) ? "\n\n" : '';

self.setupTracking('redir', pageTitles.length);

// Make a redirect var apiMakeRedirect = function(pageTitle, isRetry) { var newWikitext; if ( pageTitle.indexOf('Module:') === 0 ) { var targetPage = self.inputData.getTargetLink(pageTitle, true); if ( targetPage.indexOf('Module:') !== 0 ) { self.track('redir', false); self.addError([					'Could not redirect ',					extraJs.makeLink(pageTitle),					' because target (', extraJs.makeLink(targetPage), ') is not a module'				], true); return false; }			newWikitext = 'return require( "' + targetPage + '" )'; } else if ( softRedirect ) { newWikitext = '' + rcatshell; } else { newWikitext = "#REDIRECT " + self.inputData.getTargetLink(pageTitle) + rcatshell; }		API.postWithToken( 'csrf', {			action: 'edit',			title: pageTitle,			text: newWikitext,			summary: '' + self.discussion.getNomPageLink + ' closed as ' +				self.inputData.getResult + config.script.advert		} ) .done( function {			self.track('redir', true);		} ) .fail( function(code, jqxhr) {			if (!isRetry) {				apiMakeRedirect(pageTitle, true);			}			self.track('redir', false);			self.addApiError(code, jqxhr, [ 'Could not edit page ', extraJs.makeLink(pageTitle) ]);		} );	};

// Delete before redirecting var apiDelAndRedir = function(pageTitle, isRetry) { API.postWithToken( 'csrf', {			action: 'delete',			title: pageTitle,			reason:  + self.discussion.getNomPageLink +  + config.script.advert		} ) .done( function {			apiMakeRedirect(pageTitle);		} ) .fail( function(code, jqxhr) {			if (!isRetry) {				apiDelAndRedir(pageTitle, true);			}			self.track('redir', false);			self.addApiError(code, jqxhr, [ 'Could not delete page ', extraJs.makeLink(pageTitle) ]);		} );	};	// For each page, check that it isn't a target, then redirect, or delete and redirect for ( var i=0; i<pageTitles.length; i++ ) { if ( $.inArray(pageTitles[i], self.inputData.getTargetsArray('redirect')) !== -1 ) { //Skip (don't make a page redirect to itself) self.track('redir', false); } else if ( deleteFirst ) { apiDelAndRedir(pageTitles[i]); } else { apiMakeRedirect(pageTitles[i]); }	} };

Task.prototype.doTask.removeCircularLinks = function(self) {

// Notify task is started self.setStatus('started');

var pageTitles = self.discussion.getPageTitles; var targetTitles = self.inputData.getTargetsArray('redirect'); self.setupTracking('uncircle', targetTitles.length);

// Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { var oldWikitext = page.content; // Don't remove selflinks var unlinkPageTitles = pageTitles.filter(function(t){			return t !== page.title;		}); var newWikitext = extraJs.unlink(oldWikitext, unlinkPageTitles); // Check if any changes need to be made; if redirect target is a nominated page, remove // nomination template and adjust edit summary if ( newWikitext === oldWikitext ) { // No links to unlink return $.Deferred.reject("noCircularRedirects"); }		if ( $.inArray(page.title, pageTitles) !== -1 ) { // Target is one of the nominated pages - also try to remove nom template if present try { newWikitext = config.xfd.removeNomTemplate(newWikitext); } catch(e){ // Warning if multiple nom templates found return $.Deferred.reject("multipleNomTemplates", e); }						}		return { text: newWikitext, summary: 'Unlinking circular redirects: ' + self.discussion.getNomPageLink +				' closed as ' + self.inputData.getResult + config.script.advert };	};	API.editWithRetry(		targetTitles,		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "noCircularRedirects":					self.addWarning('none found');					self.track('uncircle', false);					break;				case "multipleNomTemplates":					self.addError([ error.message + ' ', extraJs.makeLink(title) ]);					self.track('uncircle', false);					break;				default:					self.addApiError(code, error, [ 'Could not edit page ', extraJs.makeLink(title) ]);					self.track('uncircle', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of redirect target' + ( targetTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

// --- Relisting tasks --- Task.prototype.doTask.getRelistInfo = function(self) {

// Notify task is started self.setStatus('started'); self.setupTracking('done', 1);

var now = new Date; var today = now.getUTCFullYear + ' ' + config.monthNames[now.getUTCMonth] + ' ' + now.getUTCDate; var todaysLogpage = config.xfd.path + today; // Set log page link in task message if ( config.xfd.type !== 'mfd' ) { self.discussion.taskManager.getTaskByName('updateNewLogPage').setDescription([			'Adding to ',			extraJs.makeLink(todaysLogpage, "today's log page")		]); }	var query = { action: 'query', titles: self.discussion.nomPage, prop: 'revisions', indexpageids: 1, rawcontinue: 1, rvprop: 'content|timestamp', rvslots: 'main', rvsection: self.discussion.sectionNumber, curtimestamp: 1 };	if ( config.xfd.type === 'afd' ) { $.extend(query, { 			list: 'embeddedin',			eititle: self.discussion.nomPage,			einamespace: config.xfd.ns_logpages,			eifilterredir: 'nonredirects',			eilimit: 500		}); // Need to fetch whole page, in order to check if afd is already closed delete query.rvsection; }	API.get( query ) .then( function (result) {		// Discussion wikitext		var id = result.query.pageids;		var oldWikitext = result.query.pages[ id ].revisions[0].slots.main['*'];		var heading = oldWikitext.slice(0, oldWikitext.indexOf('\n'));

// Abort if discussion is already closed if ( oldWikitext.indexOf('xfd-closed') !== -1 ) { self.discussion.taskManager.abortTasks('discussion has been closed'); return; }		// Relist template var relists = oldWikitext .match(/\[\[Wikipedia:Deletion process#Relisting discussions\|Relisted\]\]/g); var relistTemplate = '\n' + self.inputData.getRelistComment + '\n'; // New/relisted discussion var newWikitext = oldWikitext.trim + relistTemplate; // Wikitext for old log page var oldLogWikitext = ''; if ( config.xfd.type === 'afd' ) { // Update link to log page newWikitext = newWikitext.replace(				/\[\[Wikipedia:Articles for deletion\/Log\/\d{4} \w+ \d{1,2}#/,				'| config.xfd.type === 'tfd' ) {			// Discussion on old log page gets closed			var xfdCloseTop = config.xfd.wikitext.closeTop				.replace(/__RESULT__/, 'relisted')				.replace(/__TO_TARGET__/, ' on [[' + todaysLogpage + '#' +					self.discussion.sectionHeader + '|' + today + )				.replace(/__RATIONALE__/, '.')				.replace(/__SIG__/, config.user.sig);			// List of nominated pages			var pagesList = ;			if ( !self.discussion.isBasicMode ) {				pagesList = self.discussion.pages.reduce(function(list, page) {					var namespaceParam = ( page.getNamespaceId === 828 ) ? '|module=Module' : ;					return list + config.xfd.wikitext.pagelinks.replace('__PAGE__',	page.getMain + namespaceParam);				}, );			}			oldLogWikitext = heading + '\n' + xfdCloseTop + '\n' +			pagesList + config.xfd.wikitext.closeBottom;		} else if ( config.xfd.type === 'mfd' ) {			// Find first linebreak after last pagelinks templats			var splitIndex = newWikitext.indexOf('\n', newWikitext.lastIndexOf(':{{pagelinks'));			// Add time stamp for bot to properly relist			newWikitext = (newWikitext.slice(0, splitIndex).trim +			'\n{{subst:mfdr}}\n' + newWikitext.slice(splitIndex+1).trim);		} else if ( config.xfd.type === 'rfd' ) {			var topWikitext = '====' + self.discussion.sectionHeader + '====';			if ( oldWikitext.indexOf('*<span id=') !== oldWikitext.lastIndexOf('*<span id=') ) {				// Multiple redirects were nominted, so keep nominated redirects' anchors				// Find linebreak prior to first span with an id				var splitIndex1 = oldWikitext.indexOf('\n', oldWikitext.indexOf('*<span id=')-2);				// Find linebreak after the last span with an id				var splitIndex2 = oldWikitext.indexOf('\n', oldWikitext.lastIndexOf('*<span id='));				var rfdAnchors = oldWikitext.slice(splitIndex1, splitIndex2)					.replace(/\*<span/g, '<span') // remove bullets					.replace(/^(?!<span).*$\n?/gm, ) // remove lines which don't start with a span					.replace(/>.*$\s*/gm, '> ') // remove content within or after span					.trim;				topWikitext += '\n ' + rfdAnchors + ' ';			}			oldLogWikitext = topWikitext + '\n' + self.discussion.sectionHeader + ;		} else if ( config.xfd.type === 'cfd' ) {			oldLogWikitext = '====' + self.discussion.sectionHeader + '====' +			'\n' + self.discussion.sectionHeader + ;		}		// Store wikitext in task manager		self.discussion.taskManager.relistInfo = {			'today': today,			'newWikitext': newWikitext,			'oldLogWikitext': oldLogWikitext,			'nomPageTimestamps': {				start: result.curtimestamp,				base: result.query.pages[ id ].revisions[0].timestamp			}		};		// For AfDs, check that it's still transcluded to old log page...				if ( config.xfd.type === 'afd' ) {			// Get array of "embeddedin" pages which are logpages (should only be one)			var eiLogpages = result.query.embeddedin.filter(function(ei) {				return ei.title.indexOf(config.xfd.path) !== -1;			});			// Abort if none found			if ( eiLogpages.length === 0 ) {				self.addError('Old log page not found');				self.discussion.taskManager.abortTasks();				return;			}			// Warn if multiple log pages were found			if ( eiLogpages.length > 1 ) {				for (var i = 1; i < eiLogpages.length; i++) {					self.addWarning([						'Note: transcluded on additional log page: ',						extraJs.makeLink(							eiLogpages[i].title,							eiLogpages[i].title.replace(config.xfd.path, )						)					]);				}			}			// Set old log page 			var oldLogpage = eiLogpages[0];			// Abort if old log page is actually today's logpage			if ( oldLogpage.title === todaysLogpage ) {				self.addError('Already transcluded to today\'s log page');				self.discussion.taskManager.abortTasks();				return;			}			// Add old log page link in task message			self.discussion.taskManager.getTaskByName('updateOldLogPage').setDescription([				'Removing from ',				extraJs.makeLink(oldLogpage.title, 'old log page')			]);			// Store old log title			self.discussion.taskManager.relistInfo.oldlogtitle = oldLogpage.title;			self.discussion.taskManager.relistInfo.newLogEditType = 'text';			// Get logpages' wikitext			API.get( {				action: 'query',				titles: oldLogpage.title + '|' + todaysLogpage,				prop: 'revisions',				rvprop: 'content|timestamp',				rvslots: 'main',				indexpageids: 1,				rawcontinue: ,				"curtimestamp": 1			} )			.then( function(result) {				// Get wikitext				var newlogtext = ;				var oldlogtext = ;				var ids = result.query.pageids;				// Check how many log pages were found				if ( ids.length === 1 ) {					// Abort if only one log page found					self.discussion.taskManager.abortTasks(						'discussion already transcluded to today\'s log page'					);					return;				}				var newLogBaseTimestamp, oldLogBaseTimestamp;				// Identify log pages				if ( result.query.pages[ ids[0] ].title === todaysLogpage ) {					// ids[0] is the new log page					newlogtext = result.query.pages[ ids[0] ].revisions[0].slots.main['*'];					newLogBaseTimestamp = result.query.pages[ ids[0] ].revisions[0].timestamp;					oldlogtext = result.query.pages[ ids[1] ].revisions[0].slots.main['*'];					oldLogBaseTimestamp = result.query.pages[ ids[1] ].revisions[0].timestamp;				} else {					// ids[0] is the old log page					newlogtext = result.query.pages[ ids[1] ].revisions[0].slots.main['*'];					newLogBaseTimestamp = result.query.pages[ ids[1] ].revisions[0].timestamp;					oldlogtext = result.query.pages[ ids[0] ].revisions[0].slots.main['*'];					oldLogBaseTimestamp = result.query.pages[ ids[0] ].revisions[0].timestamp;				}				// Abort if already relisted				var t = mw.util.escapeRegExp(self.discussion.nomPage);				var oldPatt = new RegExp(, 'i');				var newPatt = new RegExp('\\{\\{' + t + '\\}\\}', 'i');				if ( oldPatt.test(oldlogtext) || newPatt.test(newlogtext) ) {					self.discussion.taskManager.abortTasks('discussion has been relisted already');					return;				}				// Updated new log wikitext:				var newlogreg = new RegExp(,'i');				self.discussion.taskManager.relistInfo.newLogWikitext = newlogtext.replace(					newlogreg,					'\n'				);				self.discussion.taskManager.relistInfo.newLogTimestamps = {					start: result.curtimestamp,					base: newLogBaseTimestamp				};							// Updated old log wikitext:				var oldlogreg = new RegExp("(\\{\\{" + t + "\\}\\})", 'i' );				self.discussion.taskManager.relistInfo.oldlogTransclusion = oldlogreg.test(oldlogtext);				self.discussion.taskManager.relistInfo.oldLogWikitext = oldlogtext.replace(					oldlogreg, );					self.discussion.taskManager.relistInfo.oldLogTimestamps = {					start: result.curtimestamp,					base: oldLogBaseTimestamp				};				// Ready to proceed to next tasks				self.track('done', true);			}, function(code, jqxhr) {				self.addApiError(code, jqxhr, 'Could not read contents of log pages');				self.discussion.taskManager.abortTasks();			} );		} else if ( config.xfd.type === 'tfd' || config.xfd.type === 'rfd' || config.xfd.type === 'cfd' ) {			//New discussions on top of log page, so need to check out current log page wikitext			API.get( {				action: 'query',				titles: todaysLogpage,				prop: 'revisions',				rvprop: 'content|timestamp',				rvslots: 'main',				indexpageids: 1,				rawcontinue: 			} )			.then(function(response) {				var page = pageFromResponse(response);				var logWikitext = page.revisions[ 0 ].slots.main['*'];				var h4_match = /====\s*(.*?)\s*====/.exec(logWikitext);				var h4 = h4_match && h4_match[1];				if ( h4 ) {					// there is at least 1 level 4 heading on page - can edit section #2					self.discussion.taskManager.relistInfo.newLogSection = 2;					if ( h4.toUpperCase === "NEW NOMINATIONS" ) {						self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';					} else {						self.discussion.taskManager.relistInfo.newLogEditType = 'prependtext';					}				} else {					// there are no level 4 headings on page - can append to section #1					self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';					self.discussion.taskManager.relistInfo.newLogSection = 1;

}				self.discussion.taskManager.relistInfo.newLogTimestamps = { start: response.curtimestamp, base: page.revisions[ 0 ].timestamp };				// Ready to proceed to next tasks self.track('done', true); }, function(code, jqxhr) { self.addApiError(code, jqxhr, [					'Could not read contents of ',					extraJs.makeLink(todaysLogpage, "today's log page")				]); self.discussion.taskManager.abortTasks(''); } );		} else { // ffd, mfd			self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';			// Ready to proceed to next task(s)			self.track('done', true);		}	}, function(code, jqxhr) {		self.addApiError(code, jqxhr, [	'Could not read contents of page ', extraJs.makeLink(self.discussion.nomPage), '; could not relist discussion'	] );		self.discussion.taskManager.abortTasks('');	} ); };

Task.prototype.doTask.updateDiscussion = function(self) {

// Notify task is started self.setStatus('started'); self.setupTracking('edit', 1); var relistInfo = self.discussion.taskManager.relistInfo; var params = { action: 'edit', title: self.discussion.nomPage, text: relistInfo.newWikitext, summary: 'Relisting discussion' + config.script.advert, // Protect against errors and conflicts assert: 'user', basetimestamp: relistInfo.nomPageTimestamps.base, starttimestamp: relistInfo.nomPageTimestamps.start };	if ( config.xfd.type === 'mfd' ) { params.section = self.discussion.sectionNumber; }	API.postWithToken( 'csrf', params ) .done( function {		self.track('edit', true);	} ) .fail( function(code, jqxhr) {		self.track('edit', false);		self.addApiError(code, jqxhr, 'Could not edit ' + config.xfd.type.toUpperCase + ' discussion');	} ); };

Task.prototype.doTask.updateOldLogPage = function(self) {

// Notify task is started self.setStatus('started'); self.setupTracking('edit', 1); var relistInfo = self.discussion.taskManager.relistInfo; var params = { action: 'edit', title: ( config.xfd.type === 'afd' ) ? relistInfo.oldlogtitle : self.discussion.nomPage, text: relistInfo.oldLogWikitext, summary: (( config.xfd.type === 'afd' ) ? 'Relisting ' + self.discussion.nomPage +			 : '/* ' + self.discussion.sectionHeader + ' */ Relisted on ' + relistInfo.today + ) + config.script.advert };	if (relistInfo.oldLogTimestamps) { params.basetimestamp = relistInfo.oldLogTimestamps.base; params.starttimestamp = relistInfo.oldLogTimestamps.start; }	if ( config.xfd.type === 'afd' ) { // Skip if transclusion wasn't found if ( !relistInfo.oldlogTransclusion ) { self.track('edit', false); self.addError('Transclusion not found on old log page; could not be commented out'); return; }	} else { params.section = self.discussion.sectionNumber; }	API.postWithToken( 'csrf', params ) .done( function {		self.track('edit', true);	} ) .fail( function(code, jqxhr) {		self.track('edit', false);		self.addApiError(code, jqxhr, 'Could not edit old ' + config.xfd.type.toUpperCase + ' log page');	} ); };

Task.prototype.doTask.updateNewLogPage = function(self) {

// Notify task is started self.setStatus('started'); self.setupTracking('edit', 1); var relistInfo = self.discussion.taskManager.relistInfo; if ( relistInfo.newLogEditType === 'appendtext' ) { relistInfo.newWikitext = '\n' + relistInfo.newWikitext; }	var params = { action: 'edit', title: config.xfd.path + relistInfo.today, summary: 'Relisting ' + (( config.xfd.type === 'afd' ) ?  + self.discussion.nomPage +		 : '"' + self.discussion.sectionHeader + '"') + config.script.advert };	if ( config.xfd.type === 'afd' ) { params.text = relistInfo.newLogWikitext; } else { params[relistInfo.newLogEditType] = relistInfo.newWikitext; }	if (relistInfo.newLogTimestamps) { params.basetimestamp = relistInfo.newLogTimestamps.base; params.starttimestamp = relistInfo.newLogTimestamps.start; }		if ( /(tfd|rfd|cfd)/.test(config.xfd.type) ) { params.section = relistInfo.newLogSection; }	API.postWithToken( 'csrf', params ) .done( function {		self.track('edit', true);	} ) .fail( function(code, jqxhr) {		self.track('edit', false);		self.addApiError(code, jqxhr, 'Could not edit today\'s ' + config.xfd.type.toUpperCase + ' log page');	} ); };

Task.prototype.doTask.updateNomTemplates = function(self) {

// Notify task is started self.setStatus('started');

var relistInfo = self.discussion.taskManager.relistInfo; //var todayParts = relistInfo.today.split(' '); var pageTitles = self.discussion.getPageTitles(null, {'moduledocs':true}); self.setupTracking('edit', pageTitles.length); // Function to transform a simplified API page object into edit parameters for API.editWithRetry var transform = function(page) { // Check there's a corresponding nominated page var pageObj = self.discussion.getPageByTitle(page.title, {'moduledocs': true}); if ( !pageObj ) { return $.Deferred.reject("unexpectedTitle"); }		// Check corresponding page exists if ( page.missing ) { return $.Deferred.reject("doesNotExist"); }		var originalWikitext = page.content; var updatedWikitext; try { updatedWikitext = config.xfd.updateNomTemplateAfterRelist(				originalWikitext,				relistInfo.today,				self.discussion.sectionHeader			); } catch(e){ return $.Deferred.reject("couldNotUpdate", e); }		var noChangesToMake = updatedWikitext === originalWikitext; if ( noChangesToMake ) { return $.Deferred.reject("nominationTemplateNotFound"); }		return { text: updatedWikitext, summary: 'Updating ' + config.xfd.type.toUpperCase + ' template: discussion was relisted' + config.script.advert };	};	API.editWithRetry(		pageTitles,		null,		transform,		function { self.track('edit', true); },		function(code, error, title) {			switch(code) {				case "unexpectedTitle":					self.addError([ 'API query result included unexpected title ', extraJs.makeLink(title), '; this talk page will not be edited' ]);					self.track('edit', false);					break;				case "doesNotExist":					self.addError([ extraJs.makeLink(title), ' does not exist, and will not be edited' ]);					self.track('edit', false);					break;				case "couldNotUpdate":					self.addError([ 'Could not update ', extraJs.makeLink(title), ': ',						error.message ]);					self.track('edit', false);					break;				case "nominationTemplateNotFound":					self.addWarning([ 'Nomination template not found on page ', extraJs.makeLink(title) ]);					self.track('edit', false);					break;				default:					self.addApiError(code, error, [ 'Could not edit page ', extraJs.makeLink(title) ]);					self.track('edit', false);			}		}	).fail( function(errortype, code, jqxhr) {		if ( errortype === "read" ) {			self.addApiError(code, jqxhr, 'Could not read contents of nominated page' + ( pageTitles.length > 1 ) ? 's' : '');			self.setStatus('failed');		} // other errors handled above	} ); };

/* ========== ShowHideTag class ================================================================= The 'tag' at the bottom of screen that toggles the visibility of closed discussions. -- */ // Constructor var ShowHideTag = function { // Determine previous state from localStorage try { if ( !!window.localStorage.getItem('xfdc-closedHidden') ) { this.isHidden = true; } else { this.isHidden = false; }	} catch(e) { // If localStorage not available, default to not hidden this.isHidden = false; } };

// -- ShowHideTag prototype - */ ShowHideTag.prototype.hideClosed = function { this.isHidden = true; try { window.localStorage.setItem('xfdc-closedHidden', true); } catch(e) {} $('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').hide; $('#XFDcloser-showhide-show').show; };

ShowHideTag.prototype.showClosed = function { this.isHidden = false; try { window.localStorage.setItem('xfdc-closedHidden', ''); } catch(e) {} $('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').show; $('#XFDcloser-showhide-show').hide; }; ShowHideTag.prototype.initialise = function { var self = this; $(' ')	.attr('id', 'XFDcloser-showhide') .append(		$('<a>')		.attr('id', 'XFDcloser-showhide-hide')		.text('Hide closed discussions')		.toggle(!self.isHidden)		.on('click', self.hideClosed),		$('<a>')		.attr('id', 'XFDcloser-showhide-show')		.text('Show closed discussions')		.toggle(self.isHidden)		.on('click', self.showClosed)	) .appendTo('body'); }; /* ========== Get started ======================================================================= */ // Initialise show/hide closed discussions tag, unless there is only one discussion on the page if ( $('#mw-content-text ' + config.xfd.html.head).length > 1 ) { config.showHide = new ShowHideTag; config.showHide.initialise; }

// Set up discussion object for each discussion $(config.xfd.html.head + ' > span.mw-headline') .not('.XFDcloser-ignore') .each(function(i) {	var d = Discussion.newFromHeadlineSpan(i, this);	if ( d ) {		try {			if ( d.isBasicMode ) {				d.showLinks;			} else if (d.pages.length > 50 && !config.user.isSysop) {				d.setStatus( extraJs.addTooltip(						$(" ")							.addClass("xfdc-status")							.css({"color":"#9900A2"})							.text("[Too many pages]"),						"Only administrators can close or relist discussions with more than "+						"50 nominated pages using XFDcloser."					) );			} else {				d.retrieveExtraInfo				.then( function { d.showLinks; }, function(failMessage) { // set "basic" mode, show "basic" links with the failure message d.pages = false; d.showLinks(failMessage); }				);			}		} catch(e) {			console.warn('[XFDcloser] Could not retrieve page info for ' + $(this).text + ' [see error below]');			console.warn(e);		}	} });

// If showHide state is hidden, hide any headings that may have had class 'xfd-closed' added if ( config.showHide && config.showHide.isHidden ) { config.showHide.hideClosed; }

/* ========== End of full file closure wrappers ================================================ */ }); /* */