MediaWiki:Gadget-zglos-do-wyroznienia-wzw.js

W tym artykule zajmiemy się tematem MediaWiki:Gadget-zglos-do-wyroznienia-wzw.js, który w ostatnich latach zyskał na znaczeniu ze względu na jego wpływ na różne obszary. MediaWiki:Gadget-zglos-do-wyroznienia-wzw.js był przedmiotem debaty i analizy ekspertów w tej dziedzinie, którzy podkreślili znaczenie zrozumienia i refleksji nad jego konsekwencjami. W tym artykule przeanalizujemy różne perspektywy i badania związane z MediaWiki:Gadget-zglos-do-wyroznienia-wzw.js, mając na celu przedstawienie kompleksowego i aktualnego spojrzenia na ten temat. Podobnie zbadamy jego wpływ na społeczeństwo, gospodarkę, politykę i inne istotne aspekty, aby zrozumieć jego zakres i wpływ w obecnym kontekście.
//@ts-check
/**
 * @author ]
 * 
 * Skrypt do nominacji artykułów do wyróżnień AnM, DA i LnM.
 * 
 * <nowiki>
 */
(function ($, mw) {
	var wzwMSG = {
		justificationLabel: 'Uzasadnienie',
		justificationHelp: 'Opisz, dlaczego, Twoim zdaniem, artykuł nadaje się do wybranego wyróżnienia (a w przypadku zgłoszenia do weryfikacji – dlaczego się nie nadaje). Twój podpis na stronie nominacji zotanie wstawiony automatycznie.',
		authorsLabel: 'Główni autorzy',
		authorsHelp: '<span>Tutaj możesz wpisać nazwy użytkowników, którzy mają zostać powiadomieni o nominacji. Zatwierdź klawiszem Tab lub Enter. Lista autorów znajduje się w <a href="/w/index.php?title=$1&action=history" target="_blank">historii strony</a>. Możesz też posłużyć się <a href="https://xtools.wmflabs.org/articleinfo/pl.wikipedia.org/$1" target="_blank">szczegółowymi statystykami</a>.</span>',
		wikiprojectsLabel: 'Wikiprojekty do powiadomienia',
		wikiprojectsHelp: 'Powiadomienie o nominacji zostanie umieszczone na stronach wyżej wymienionych wikiprojektów.',
		justificationEmpty: 'Pole „Uzasadnienie” nie może być puste.',

		nominationCreateSummary: 'Nowe zgłoszenie do wyróżnienia',
		nominationCreateSummaryRevoke: 'Nowe zgłoszenie do weryfikacji wyróżnienia',
		nominationMarker: 'Uzasadnienie:\n',
		lobbyEditSummary: 'Dodano nowe zgłoszenie: ]',
		lobbyEditSummaryRevoke: 'Dodano nowe zgłoszenie do weryfikacji: ]',
		authorTalkSubject: 'Artykuł ] został zgłoszony do wyróżnienia',
		authorTalkSubjectRevoke: 'Artykuł ] został zgłoszony do weryfikacji wyróżnienia',
		authorTalkMessage: '$1\nPozdrawiam, ~~~~',
		articleEditSummary: 'Artykuł został zgłoszony do wyróżnienia: ]',
		articleEditSummaryRevoke: 'Artykuł został zgłoszony do weryfikacji wyróżnienia: ]',
		wikiprojectMessage: '$1\n~~~~',
		wikiprojectMarker: '<!-- Nowe nominacje wstawiaj poniżej tej linii. Nie zmieniaj ani nie kasuj tej linii. -->',
		wikiprojectBeforeMarker: '<!-- Poniższa sekcja została utworzona przez gadżet „Zgłoś do wyróżnienia”. Możesz ją swobodnie przenosić, a także zmieniać tekst nagłówka. -->\n=== Propozycje wyróżnień ===\n',
		wikiprojectNotifySummary: 'Artykuł ] został zgłoszony do wyróżnienia',
		wikiprojectNotifySummaryRevoke: 'Artykuł ] został zgłoszony do weryfikacji wyróżnienia',
		
		invalidTitle: 'Strona nominacji miałaby niepoprawny tytuł. Gadżet nie jest w stanie obsłużyć takiej sytuacji.',
		canFindMarkerInLobby: '<!-- Gadżet nie był w stanie zlokalizować właściwego miejsca do wstawienia tego zgłoszenia. -->',
		errorCantMakeTitle: 'Nie udało się określić tytułu dla strony nominacji. $1',
		errorCantCreatePage: 'Nie udało się utworzyć strony nominacji. $1',
		errorCantAddToLobby: 'Strona nominacji została utworzona, ale nie udało się osadzić zgłoszenia na stronie propozycji. $1',
		errorCantAddTemplate: 'Nominacja została wykonana, ale nie udało się oznaczyć artykułu szablonem propozycji wyróżnienia. Nie powiadomiono też autorów artykułu. $1',
		errorCantNotifyUsers: 'Nominacja została wykonana, ale nie udało się powiadomić użytkowników. $1',
		errorCantNotifyWikiprojects: 'Nominacja została wykonana, ale nie udało się powiadomić wikiprojektów. $1',

		apiFormatError: 'Serwer odpowiedział w nieprawidłowym formacie. Szczegóły błędu być może znajdują się w konsoli w narzędziach deweloperskich (zazwyczaj dostępnych po naciśnięciu F12).',
	};

	// Defines who is regarded as a 'main author' of an article.
	// In the last 60 days, the user did changes whose magnitudes sum to over 5000 bytes.
	var AUTHORS = {
		days: 60,
		totalEditSize: 5000
	};

	/** @type {NominationTypeWZW} */
	var NOMINATION_TYPES = [
		{
			sortOrder: 1,
			label: 'Dobry Artykuł',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Propozycja_DA.svg/15px-Propozycja_DA.svg.png',
			lobbyMarker: /(== Propozycje ==\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do Dobrych Artykułów',
			preload: 'Wikipedia:Propozycje do Dobrych Artykułów/preload',
			talkTemplate: '{{Propozycja wyróżnienia|DA$1|artykuł=$2}}',
			template: '{{Propozycja wyróżnienia|DA$1}}',
			transform: function (content) {
				return content.replace('<!-- Powód, dlaczego uważasz, że zasługuje na miano Dobrego Artykułu. (Ewentualnie pewne mankamenty, które nie pozwalają, aby hasło mogło być uznane za medalowe).-->', '');
			},
			isRevoke: false,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Artykuł nie może być zgłoszony do DA, jeśli posiada już jakieś wyróżnienie lub jest nominowany do jakiegokolwiek
				if(categories.indexOf('Artykuły zgłoszone do Dobrych Artykułów') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do List na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do Artykułów na Medal') !== -1) return false;

				if(categories.indexOf('Dobre Artykuły') !== -1) return false;
				if(categories.indexOf('Listy na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły na Medal') !== -1) return false;
				return true;
			}
		},
		{
			sortOrder: 4.1,
			label: 'Dobry Artykuł – weryfikacja',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Odebranie_DA_orange.svg/15px-Odebranie_DA_orange.svg.png',
			lobbyMarker: /(\{\{\/weryfikacja\}\}\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do Dobrych Artykułów',
			preload: 'Wikipedia:Propozycje do Dobrych Artykułów/weryfikacja/preload',
			talkTemplate: '{{Weryfikacja wyróżnienia|DA$1|artykuł=$2}}',
			template: '{{Weryfikacja wyróżnienia|DA$1}}',
			titleSuffix: '/weryfikacja',
			isRevoke: true,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Do weryfikacji DA można zgłaszać tylko DA
				return categories.indexOf('Dobre Artykuły') !== -1;
			}
		},
		{
			sortOrder: 2,
			label: 'Lista na Medal',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Propozycja_LnM-2.svg/15px-Propozycja_LnM-2.svg.png',
			lobbyMarker: /(== Propozycje ==\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do List na Medal',
			preload: 'Wikipedia:Propozycje do List na Medal/preload',
			talkTemplate: '{{Propozycja wyróżnienia|LnM$1|artykuł=$2}}',
			template: '{{Propozycja wyróżnienia|LnM$1}}',
			isRevoke: false,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Artykuł nie może być zgłoszony do LnM, jeśli posiada już wyróżnienie LnM/AnM lub jest nominowany do jakiegokolwiek
				if(categories.indexOf('Artykuły zgłoszone do Dobrych Artykułów') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do List na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do Artykułów na Medal') !== -1) return false;

				if(categories.indexOf('Listy na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły na Medal') !== -1) return false;
				return true;
			}
		},
		{
			sortOrder: 4.2,
			label: 'Lista na Medal – weryfikacja',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Odbieranie_LnM.svg/15px-Odbieranie_LnM.svg.png',
			lobbyMarker: /(\{\{\/weryfikacja\}\}\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do List na Medal',
			preload: 'Wikipedia:Propozycje do List na Medal/weryfikacja/preload',
			talkTemplate: '{{Weryfikacja wyróżnienia|LnM$1|artykuł=$2}}',
			template: '{{Weryfikacja wyróżnienia|LnM$1}}',
			titleSuffix: '/weryfikacja',
			isRevoke: true,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Do weryfikacji LnM można zgłaszać tylko LnM
				return categories.indexOf('Listy na Medal') !== -1;
			}
		},
		{
			sortOrder: 3,
			label: 'Artykuł na Medal',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Wikimedal_POL.svg/12px-Wikimedal_POL.svg.png',
			lobbyMarker: /(== Propozycje ==\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do Artykułów na Medal',
			preload: 'Wikipedia:Propozycje do Artykułów na Medal/preload',
			talkTemplate: '{{Propozycja wyróżnienia|AnM$1|artykuł=$2}}',
			template: '{{Propozycja wyróżnienia|AnM$1}}',
			isRevoke: false,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Artykuł nie może być zgłoszony do AnM, jeśli posiada już wyróżnienie LnM/AnM lub jest nominowany do jakiegokolwiek
				if(categories.indexOf('Artykuły zgłoszone do Dobrych Artykułów') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do List na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły zgłoszone do Artykułów na Medal') !== -1) return false;

				if(categories.indexOf('Listy na Medal') !== -1) return false;
				if(categories.indexOf('Artykuły na Medal') !== -1) return false;
				return true;
			}
		},
		{
			sortOrder: 4.3,
			label: 'Artykuł na Medal – weryfikacja',
			icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/Omedal_orange.svg/15px-Omedal_orange.svg.png',
			lobbyMarker: /(\{\{\/weryfikacja\}\}\n<!-- Nowe propozycje umieszczaj pod tą linią.*?-->\n\n?)/i,
			rootPage: 'Wikipedia:Propozycje do Artykułów na Medal',
			preload: 'Wikipedia:Propozycje do Artykułów na Medal/weryfikacja/preload',
			talkTemplate: '{{Weryfikacja wyróżnienia|AnM$1|artykuł=$2}}',
			template: '{{Weryfikacja wyróżnienia|AnM$1}}',
			titleSuffix: '/weryfikacja',
			isRevoke: true,
			createView: createView,
			submit: submit,
			validateForm: validateForm,

			isApplicable: function (categories) {
				// Do weryfikacji AnM można zgłaszać tylko AnM
				return categories.indexOf('Artykuły na Medal') !== -1;
			}
		}
	];

	var viewPanel;
	var justificationInput;
	var authorsInput;
	var wikiprojectsInput;

	/**
	 * Creates a panel with the necessary controls
	 * @returns {OO.ui.PanelLayout}
	 */
	function createView() {
		if(viewPanel) return viewPanel;

		viewPanel = new OO.ui.PanelLayout({
			padded: false,
			expanded: false,
		});

		var fieldset = new OO.ui.FieldsetLayout({});
		viewPanel.$element.append(fieldset.$element);

		// The "Justification" field
		justificationInput = new OO.ui.MultilineTextInputWidget({
			rows: 5,
			indicator: 'required'
		});
		var justificationField = new OO.ui.FieldLayout(justificationInput, {
			label: wzwMSG.justificationLabel,
			align: 'top',
			help: wzwMSG.justificationHelp,
			helpInline: true
		});

		var pageName = mw.config.get('wgPageName');
		// The "Main authors" field
		authorsInput = new OO.ui.TagMultiselectWidget({
			allowArbitrary: true
		});
		var authorsField = new OO.ui.FieldLayout(authorsInput, {
			label: wzwMSG.authorsLabel,
			align: 'top',
			help: $(mw.format(wzwMSG.authorsHelp, pageName)),
			helpInline: true
		});

		// The "Wikiprojects field"
		wikiprojectsInput = new OO.ui.MenuTagMultiselectWidget({
			inputPosition: 'inline',
			options: 
		});
		var wikiprojectsField = new OO.ui.FieldLayout(wikiprojectsInput, {
			label: wzwMSG.wikiprojectsLabel,
			align: 'top',
			help: wzwMSG.wikiprojectsHelp,
			helpInline: true
		});

		fieldset.addItems([
			justificationField,
			authorsField,
			wikiprojectsField
		]);

		viewPanel.$element.append(fieldset.$element);

		getMainAuthors(pageName).then(function (authors) {
			var currentUser = mw.config.get('wgUserName');
			var usernames = Object.keys(authors);
			usernames.forEach(function (name) {
				if(name == currentUser) return;
				authorsInput.addTag(name);
			});
		}).fail(function () { });

		// The wikiprojects select is populated asynchronously
		gadget.getWikiprojects().then(function (result) {
			var wikiprojectOptions = result.wikiprojects.map(
				function (wikiproject) {
					return {
						data: wikiproject.page,
						label: wikiproject.name
					};
				}
			);
			wikiprojectsInput.addOptions(wikiprojectOptions);
		});

		return viewPanel;
	}

	/**
	 * Validates the form
	 * @returns {{isValid: boolean, messages: string}}
	 */
	function validateForm() {
		var justification = justificationInput.getValue();
		if(!justification || justification.length < 10) {
			return {
				isValid: false,
				messages: 
			};
		}
		return {
			isValid: true,
			messages: 
		};
	}

	/**
	 * Submits the form and starts the nomination
	 * 
	 * @param {NominationTypeWZW} nominationType The nomination type
	 * @returns {OO.ui.Process}
	 */
	function submit(nominationType) {
		var pageName = mw.config.get('wgPageName');
		var justification = justificationInput.getValue();
		var authors = authorsInput.getValue();
		var wikiprojects = wikiprojectsInput.getValue();

		return nominate(pageName, nominationType, justification, authors, wikiprojects);
	}

	/**
	 * Returns a promise resolving to the list of main article authors
	 * @param {string} title The title of the page
	 * @returns {JQuery.Promise<{: number}, string>}
	 */
	function getMainAuthors(title) {
		var deferred = $.Deferred();

		getRevisions(title, AUTHORS.days).then(function (revisions) {
			var contributors = {};
			for(var i = 0; i < revisions.length - 1; i++) {
				var revision = revisions;

				if(revision.anon !== undefined) continue;
				if(revision.user === 'userhidden') continue;

				var diff = Math.abs(revision.size - revisions.size);

				if(contributors === undefined)
					contributors = 0;
				contributors += diff;
			}

			Object.keys(contributors).forEach(function (user) {
				if(contributors >= AUTHORS.totalEditSize) return;
				delete contributors;
			});

			deferred.resolve(contributors);
		}).fail(function (reason) {
			deferred.reject(reason);
		});

		return deferred.promise();
	}

	/**
	 * Returns list of revisions that were made over the specified period
	 * @param {string} title The page
	 * @param {number} days How many days to take into account
	 * @returns {JQuery.Promise<any, string>}
	 */
	function getRevisions(title, days) {
		var deferred = $.Deferred();

		var limitDate = new Date();
		limitDate.setDate(limitDate.getDate() - days);

		var params = {
			'action': 'query',
			'format': 'json',
			'prop': 'revisions',
			'titles': title,
			'rvprop': ,
			'rvlimit': 'max',
			'rvend': limitDate.toISOString()
		};

		zdw.api.get(params).then(function (data) {
			var revisions = data.query.pages.revisions || ;
			deferred.resolve(revisions);
		}).fail(function (code, response) {
			var message = zdw.api.getErrorMessage(response).text();
			deferred.reject(message);
		});

		return deferred.promise();
	}

	/**
	 * Invokes all the steps in order to nominate the article
	 * @param {string} pageTitle The title of the page
	 * @param {NominationTypeWZW} nominationType Data about the nomination type
	 * @param {string} justification The reason why to feature the article
	 * @param {string} usersToNotify List of usernames whom to notify
	 * @param {string} wikiprojectsToNotify List of wikiproject pages to put a notice
	 * @returns {OO.ui.Process}
	 */
	function nominate(pageTitle, nominationType, justification, usersToNotify, wikiprojectsToNotify) {
		// Will be set by the process
		var nominationPage = '';
		var nominationNumber = 0;

		// Signature is hard-coded on the preload page
		justification = justification.replace(/~{3,5}/g, '');
		// Prepare the 'readable' page title with underscores converted to spaces
		var pageTitleReadable = pageTitle.replace(/_/g, ' ');

		return new OO.ui.Process(
			function () {
				return errorIfRejected(
					makeNominationTitle(nominationType.rootPage, pageTitle, nominationType.titleSuffix)
						.then(function (result) {
							nominationPage = result.title;
							nominationNumber = result.number;
						}),
					wzwMSG.errorCantMakeTitle
				);
			})
			.next(function () {
				var summary = wzwMSG.nominationCreateSummary;
				if(nominationType.isRevoke) summary = wzwMSG.nominationCreateSummaryRevoke;

				return errorIfRejected(
					createNominationPage(nominationType.preload, justification, nominationPage, wzwMSG.nominationMarker, summary, nominationType.transform),
					wzwMSG.errorCantCreatePage
				);
			})
			.next(function () {
				var summary = wzwMSG.lobbyEditSummary;
				if(nominationType.isRevoke) summary = wzwMSG.lobbyEditSummaryRevoke;

				return errorIfRejected(
					addNominationToLobby(nominationType.rootPage, nominationPage, nominationType.lobbyMarker, summary),
					wzwMSG.errorCantAddToLobby,
					{ recoverable: false }
				);
			})
			.next(function () {
				var nominationSuffix = '|' + nominationNumber;
				if(nominationNumber == 1) nominationSuffix = '';

				var template = mw.format(nominationType.template, nominationSuffix) + '\n';

				var summary = wzwMSG.articleEditSummary;
				if(nominationType.isRevoke) summary = wzwMSG.articleEditSummaryRevoke;
				summary = mw.format(summary, nominationPage, nominationType.label);
				return errorIfRejected(
					prependPage(pageTitle, template, summary),
					wzwMSG.errorCantAddTemplate,
					{ recoverable: false }
				);
			})
			.next(function () {
				var nominationSuffix = '|' + nominationNumber;
				if(nominationNumber == 1) nominationSuffix = '';

				var template = mw.format(nominationType.talkTemplate, nominationSuffix, pageTitleReadable);

				var subject = wzwMSG.authorTalkSubject;
				if(nominationType.isRevoke) subject = wzwMSG.authorTalkSubjectRevoke;
				subject = mw.format(subject, nominationPage, pageTitleReadable);

				var message = mw.format(wzwMSG.authorTalkMessage, template);
				return errorIfRejected(
					notifyUsers(usersToNotify, subject, message),
					wzwMSG.errorCantNotifyUsers,
					{ recoverable: false }
				);
			})
			.next(function () {
				var nominationSuffix = '|' + nominationNumber;
				if(nominationNumber == 1) nominationSuffix = '';

				var template = mw.format(nominationType.talkTemplate, nominationSuffix, pageTitleReadable);

				var summary = wzwMSG.wikiprojectNotifySummary;
				if(nominationType.isRevoke) summary = wzwMSG.wikiprojectNotifySummaryRevoke;
				summary = mw.format(summary, nominationPage, pageTitleReadable);
				return errorIfRejected(
					notifyWikiprojects(wikiprojectsToNotify, summary, template),
					wzwMSG.errorCantNotifyWikiprojects,
					{ recoverable: false }
				);
			});
	}

	/**
	 * Makes the nomination page title, regarding any potential previous nominations
	 * @param {string} nominationRootPage The lobby of nominations of the given type
	 * @param {string} articleName The article name
	 * @param {string}  The suffix to be added to the title
	 * @returns {JQuery.Promise<{title: string, number: number}, ApiError>}
	 */
	function makeNominationTitle(nominationRootPage, articleName, titleSuffix) {
		/** @type {JQuery.Deferred<{title: string, number: number}, ApiError>} */
		var deferred = $.Deferred();
		titleSuffix = titleSuffix || '';

		// Get list of all pages with the given prefix
		var firstPage = nominationRootPage + '/' + articleName + titleSuffix;
		var title = mw.Title.newFromText(firstPage);

		if(!title) {
			deferred.reject({ code: 'invalidtitle', message: wzwMSG.invalidTitle });
			return deferred.promise();
		}

		// Normalize the title
		firstPage = title.getPrefixedText();

		var params = {
			action: 'query',
			list: 'allpages',
			apnamespace: title.getNamespaceId(),
			apprefix: title.getMain(),
			aplimit: 'max'
		};
		zdw.api.get(params).then(
			function (data) {
				var pages = data.query.allpages;
				var titles = pages.map(function (page) { return page.title; });

				if(titles.indexOf(firstPage) === -1) return deferred.resolve({ title: firstPage, number: 1 });

				var i = 2;
				while(true) {
					var seqTitle = firstPage + '/' + i;
					if(titles.indexOf(seqTitle) === -1) return deferred.resolve({ title: seqTitle, number: i });
					i++;
				}
			},
			function (code, response) {
				var message = zdw.api.getErrorMessage(response).text();
				deferred.reject({ code: code, message: message });
			}
		);

		return deferred.promise();
	}

	/**
	 * Creates a page with nomination
	 * @param {string} templatePage The page with template of the nomination
	 * @param {string} justification Why should it be featured
	 * @param {string} nominationPage Where to put the nomination page
	 * @param {string} marker Where to put the justification
	 * @param {string} editSummary The edit summary to use
	 * @param {((content: string) => string) | undefined} transform An additional transformation to apply to the nomination page
	 * @returns {JQuery.Promise<void, ApiError>}
	 */
	function createNominationPage(templatePage, justification, nominationPage, marker, editSummary, transform) {
		/** @type {JQuery.Deferred<any, ApiError>} */
		var deferred = $.Deferred();

		var preloadParams = {
			'action': 'query',
			'prop': 'revisions',
			'titles': templatePage,
			'rvprop': 'content',
			'rvslots': 'main',
			'rvlimit': 1
		};
		zdw.api.get(preloadParams).then(function (response) {
			var page = response.query.pages;
			var content = page.revisions.slots.main.content;
			content = stripIncludeTags(content);
			content = content.replace(marker, marker + justification + ' ');

			if(transform) content = transform(content);

			zdw.api.create(nominationPage, { summary: editSummary }, content)
				.then(
					deferred.resolve,
					function (code, response) {
						var message = zdw.api.getErrorMessage(response).text();
						deferred.reject({ code: code, message: message });
					}
				);
		}, function (code, response) {
			var message = zdw.api.getErrorMessage(response).text();
			deferred.reject({ code: code, message: message });
		});

		return deferred;
	}

	/**
	 * Adds a nomination to the lobby
	 * @param {string} lobbyPage The title of the lobby page
	 * @param {string} nominationPage The page with nomination
	 * @param {RegExp} marker Where to put the new nomination
	 * @param {string} editSummary The edit summary to use
	 * @returns {JQuery.Promise<void, ApiError>}
	 */
	function addNominationToLobby(lobbyPage, nominationPage, marker, editSummary) {
		// Cut the prefix and make the title relative in template
		var nominationPageTemplate = nominationPage;
		if(nominationPage.indexOf(lobbyPage + '/') == 0) {
			nominationPageTemplate = nominationPage.substring(lobbyPage.length);
		}

		return repeatOnEditConflict(
			appendTextToMarker,
				[
					lobbyPage,
					marker,
					'{{' + nominationPageTemplate + '}}\n',
					mw.format(editSummary, nominationPage),
					wzwMSG.canFindMarkerInLobby
				],
			3 // 3 attempts should be sufficient
		);
	}

	/**
	 * Posts a message to the given users' talk pages
	 * @param {string} users Array of users to notify
	 * @param {string} subject The message subject
	 * @param {string} message The message to post
	 * @returns {JQuery.Promise<void, string>}
	 */
	function notifyUsers(users, subject, message) {
		var promises = ;

		for(var i = 0; i < users.length; i++) {
			var userTalk = new mw.Title('User talk:' + users);
			var promise = postTopic(userTalk, subject, message);
			promises.push(promise);
		}

		return $.when.apply($, promises);
	}

	/**
	 * Posts a message to the given wikiproject talk pages
	 * @param {string} wikiprojects Array of wikiprojects to notify
	 * @param {string} editSummary The edit summary
	 * @param {string} message The message to post
	 * @returns {JQuery.Promise<void, ApiError>}
	 */
	function notifyWikiprojects(wikiprojects, editSummary, message){
		var promises = ;

		for(var i = 0; i < wikiprojects.length; i++) {
			var wikiprojectPage = wikiprojects;
			var promise = repeatOnEditConflict(
				appendTextToMarker,
				[
					wikiprojectPage,
					wzwMSG.wikiprojectMarker,
					message,
					editSummary,
					wzwMSG.wikiprojectBeforeMarker
				],
				3 // 3 attempts should be sufficient
			)
			promises.push(promise);
		}

		// Resolve our promise only after all edits are finished
		return $.when.apply($, promises);
	}

	/**
	 * Invokes the function and retries it in case of an edit conflict.
	 * Use only when it makes sense to redo the operation with the same arguments.
	 * @param {(...args: TArgs) => JQuery.Promise<TSuccess, TFail>} func The function to invoke
	 * @param {TArgs} args The function arguments
	 * @param {number} attemptCount The number of attempts to perform
	 * @returns {JQuery.Promise<TSuccess, TFail>}
	 * @template TSuccess
	 * @template {{ code: string }} TFail
	 * @template {any} TArgs
	 */
	function repeatOnEditConflict(func, args, attemptCount) {
		var deferred = $.Deferred();

		function attempt() {
			return func.apply(null, args)
				.then(
					deferred.resolve,
					function (/** @type{TFail} */ error) {
						if(error.code === 'editconflict' && attemptCount > 1) {
							attemptCount--;
							return attempt();
						}
						deferred.reject(error);
					}
				);
		}

		attempt();
		return deferred.promise();
	}

	/**
	 * Strips <includeonly> tags and removes all the text that's between <noinclude></noinclude>
	 * @param {string} content The text
	 * @returns {string}
	 */
	function stripIncludeTags(content) {
		content = content.replace(/<noinclude>.*?<\/noinclude>/gi, '');
		content = content.replace(/<includeonly>/gi, '');
		content = content.replace(/<\/includeonly>/gi, '');
		return content;
	}

	/**
	 * Posts a new topic to the specified page
	 * 
	 * @param {mw.Title} title Where to post the message
	 * @param {string} subject The subject of the message
	 * @param {string} message The message to post
	 * @returns {JQuery.Promise<any, string>}
	 */
	function postTopic(title, subject, message){
		var deferred = $.Deferred();

		mw.messagePoster.factory.create(title).then(function (poster) {
			poster.post(subject, message).then(function (data) {
				// Resolve the promise if the post was successful
				// Usually it's going to be a normal edit
				// but it may happen that the talk page is a Flow page - check both
				if(data.edit && data.edit.result === 'Success') return deferred.resolve();
				if(data.flow && data.flow.status == 'ok') return deferred.resolve();

				deferred.reject(wzwMSG.apiFormatError);
			}).fail(function (primaryError, secondaryError, details) {
				console.error(primaryError, secondaryError, details);

				if(primaryError != 'api-fail') return deferred.reject(details.toString());
				var errorInfo = zdw.api.getErrorMessage(details).text();
				deferred.reject(errorInfo);
			});
		});

		return deferred.promise();
	}

	/**
	 * Prepends the page with the given text
	 * @param {string} page The page to edit
	 * @param {string} text The text to insert at the beginning of the page
	 * @param {string} editSummary The edit summary
	 * @returns {JQuery.Promise<void, ApiError>}
	 */
	function prependPage(page, text, editSummary) {
		/** @type {JQuery.Deferred<any, ApiError>} */
		var deferred = $.Deferred();

		var params = {
			action: 'edit',
			title: page,
			prependtext: text,
			summary: editSummary
		};

		zdw.api.postWithEditToken(params).then(
			deferred.resolve,
			function (code, response) {
				var message = zdw.api.getErrorMessage(response).text();
				deferred.reject({ code: code, message: message });
			}
		);

		return deferred.promise();
	}

	/**
	 * Finds a specific marker on the page and appends to it
	 * @param {string} pageTitle The page to edit
	 * @param {string | RegExp} marker The marker to be found
	 * @param {string} textToAppend String that will be inserted just after the marker
	 * @param {string} editSummary Summary of the edit
	 * @param {string}  If the marker is not found, this text will be appended to the page content, followed by the `marker` and `textToAppend`.
	 * @returns {JQuery.Promise<void, ApiError>}
	 */
	function appendTextToMarker(pageTitle, marker, textToAppend, editSummary, beforeMarker) {
		var deferred = $.Deferred();
		beforeMarker = beforeMarker || '';

		zdw.api.edit(pageTitle, function (revision) {
			// Replace the marker with itself + the new text
			var replacement = (typeof marker == 'string') ?
				(marker + '\n' + textToAppend)
				: ('$1' + textToAppend);
			var newContent = revision.content.replace(
				marker,
				replacement
			);

			// Check if the marker was found; if not, append the text to the end
			if(newContent == revision.content) {
				newContent += '\n' + beforeMarker;
				if (typeof marker == 'string') newContent += marker + '\n';
				newContent += textToAppend;
			}

			return {
				text: newContent,
				summary: editSummary
			};
		}).then(
			deferred.resolve,
			function (code, result) {
				var errorMessage = zdw.api.getErrorMessage(result).text();
				return deferred.reject({ code: code, message: errorMessage });
			}
		);

		return deferred.promise();
	}

	/**
	 * Wraps a promise so that if it's rejected, the new promise will be rejected with
	 * OO.ui.Error as a reason.
	 * @template T
	 * @param {JQuery.Promise<T, (string | { message: string })>} promise A promise to wrap
	 * @param {string}  A messageTempate. `$1` will be replaced with the original promise's rejection reason
	 * @param {object}  Options to pass to OO.ui.Error
	 * @returns {JQuery.Promise<T, OO.ui.Error>}
	 */
	function errorIfRejected(promise, messageTemplate, errorOptions) {
		var deferred = $.Deferred();

		promise.then(function (result) {
			deferred.resolve(result);
		}, function (reason) {
			if (typeof reason === 'object') {
				reason = reason.message;
			}
			var message = mw.format(messageTemplate || '$1', reason);
			deferred.reject(new OO.ui.Error(message, errorOptions));
		});

		return deferred.promise();
	}

	// Attach to the main script
	window.zdw = window.zdw || {};
	zdw.nominationTypes = zdw.nominationTypes || ;
	zdw.nominationTypes.push.apply(zdw.nominationTypes, NOMINATION_TYPES);
})($, mw);
// </nowiki>