コンテンツにスキップ

MediaWiki:Gadget-MovePageWarnings.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/*****************************************************************************************\
	MovePageWarnings
	Generate warnings on Special:Movepage, per the states of the move destination.
	@author [[User:Dragoniez]]
	@version 1.2.0
\*****************************************************************************************/

/* eslint-disable @typescript-eslint/no-this-alias */
// @ts-check
/* global mw */
//<nowiki>

(function() {

	// Check whether we should run the script
	var moveFrom = mw.config.get('wgRelevantPageName').replace(/_/g, ' ');
	if (!(
		// User is on Special:Movepage, and
		mw.config.get('wgCanonicalSpecialPageName') === 'Movepage' &&
		// User isn't on the root of Special:Movepage, and
		moveFrom && moveFrom !== mw.config.get('wgPageName').replace(/_/g, ' ') &&
		// User has the right to move pages, and
		// @ts-ignore
		mw.config.get('wgUserGroups', []).indexOf('autoconfirmed') !== -1 &&
		// Browser is compatible with MutationObserver (we have to be able to detect changes in software-defined OOUI elements)
		MutationObserver
	)) {
		// If any of the above lacks, stop running the script
		return;
	}

	// Define main functions, using a class
	var MovePageWarnings = /** @class */ (function() {

		// Collect all localized, canonical namespace prefixes
		var wgFormattedNamespaces = mw.config.get('wgFormattedNamespaces');
		var prefixes = Object.keys(wgFormattedNamespaces).reduce(/** @param {string[]} acc */ function(acc, key) {
			var val = wgFormattedNamespaces[key];
			if (val) acc.push(val); // Except for the main namespace
			return acc;
		}, []);

		/**
		 * Sanitize a localized namespace prefix.
		 * @param {string} prefix
		 * @returns {string} An empty string if there's no match with any of the localized namespace prefixes. ("(Main)" -> "")
		 */
		var sanitizePrefix = function(prefix) {
			return prefixes.some(function(pfx) { return pfx === prefix; }) ? prefix : '';
		};

		// Create namespace alias regex
		var wgNamespaceIds = mw.config.get('wgNamespaceIds');
		var aliases = Object.keys(wgNamespaceIds).reduce(/** @param {string[]} acc */ function(acc, alias) {
			if (alias) acc.push(alias.replace(/_/g, '[_ ]'));
			return acc;
		}, []);
		var rWhitespaceStr = '[ _\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*'; // Stringified `\s`, including underscore and excluding tab
		var rAliases = new RegExp('^' + rWhitespaceStr + '(' + aliases.join('|') + ')' + rWhitespaceStr + ':', 'i');

		/**
		 * Regex for unicode bidirectional characters (from `MediaWikiTitleCodec::splitTitleString()` in PHP).
		 */
		var rUnicodeBidi = /[\u200E\u200F\u202A-\u202E]+/g;

		/**
		 * Initialize a MovePageWarnings instance.
		 *
		 * @constructor
		 * @param {Element} prefixLabel
		 * @param {HTMLInputElement} titleInput
		 * @param {JQuery<HTMLElement>} $submitButton
		 */
		function MovePageWarnings(prefixLabel, titleInput, $submitButton) {

			MovePageWarnings.addStyleTag();

			// Define class properties

			/**
			 * The page name of the moving target (the "from" page name).
			 * @type {string}
			 * @readonly
			 */
			this.target = moveFrom;

			/**
			 * The value selected in the namespace selector dropdown (updated in the callback of MutationObserver).
			 * @type {string}
			 */
			this.prefix = sanitizePrefix(prefixLabel.innerHTML);

			/**
			 * The input tag in the OOUI InputWidget used as a wgTitle specifier.
			 * @type {HTMLInputElement}
			 * @readonly
			 */
			this.titleInput = titleInput;

			/**
			 * The span tag in the OOUI button for form submission.
			 * @type {JQuery<HTMLSpanElement>}
			 * @readonly
			 */
			this.$submitButton = $submitButton;

			/**
			 * The "move associated talk page" button.
			 * @type {HTMLInputElement?}
			 * @readonly
			 */
			this.moveTalkBox = document.querySelector('#wpMovetalk > input');

			/**
			 * Stores the page name of the move destination last inputted.
			 * @type {string}
			 */
			this.lastPagename = moveFrom;

			/**
			 * Whether the current user can delete pages.
			 * @type {boolean}
			 * @readonly
			 */
			// @ts-ignore
			this.candelete = mw.config.get('wgUserGroups', []).concat(mw.config.get('wgGlobalGroups', [])).some(function(group) {
				return ['eliminator', 'sysop', 'interface-admin', 'global-deleter', 'staff', 'steward', 'sysadmin'].indexOf(group) !== -1;
			});

			/** @type {mw.Api} @readonly */
			this.api = new mw.Api();

			// Watch the move destination specifiers
			var _this = this;
			var inputTimeout;
			/**
			 * The input event handler.
			 * @param {boolean} [moveTalkChanged]
			 * @param {boolean} [noTimeout]
			 */
			var initWarnings = function(moveTalkChanged, noTimeout) {
				var mtc = !!moveTalkChanged;
				clearTimeout(inputTimeout);
				inputTimeout = setTimeout(function() {
					_this.updateWarnings(mtc);
				}, noTimeout ? 0 : 1000);
			};

			// Event listener for changes in the namespace prefix
			new MutationObserver(function(mutations) {
				var val = mutations[1] && mutations[1].addedNodes[0] && mutations[1].addedNodes[0].nodeValue;
				if (val) {
					_this.prefix = sanitizePrefix(val); // Update prefix
					initWarnings();
				}
			}).observe(prefixLabel, {
				childList: true,
				subtree: true
			});

			// Event listener for changes in the title
			this.titleInput.addEventListener('input', function() {
				initWarnings();
			});

			// Event listener for changes in "move associated talk page"
			if (this.moveTalkBox) {
				this.moveTalkBox.addEventListener('change', function() {
					initWarnings(true);
				});
			}

			/**
			 * The wrapper div for warning messages.
			 * @type {JQuery<HTMLDivElement>}
			 */
			this.$warning = $('<div>');

			/**
			 * The warning message list.
			 * @type {JQuery<HTMLOListElement>}
			 */
			this.$warningList = $('<ol>');

			// Append the warning wrapper to the DOM
			$('.mw-body-content').children('h2').eq(0).before(
				this.$warning
					.addClass('mw-message-box mw-message-box-warning')
					.prop('id', 'mpw-warnings')
					.hide()
					.append(
						$('<span>').append(
							$('<b>').text('警告:'),
							document.createTextNode(' 移動先ページについて、以下の点を確認してください。('),
							$('<a>')
								.prop({
									id: 'mpw-warnings-reload',
									href: '#',
									role: 'button'
								})
								.text('更新')
								.off('click').on('click', function(e) {
									e.preventDefault();
									_this.clearWarnings();
									_this.lastPagename = '';
									initWarnings(false, true);
								}),
							document.createTextNode(')')
						),
						this.$warningList
							.prop('id', 'mpw-warnings-list')
					)
			);

			initWarnings(false, true);

		}

		/**
		 * Load dependent modules and call the constructor.
		 * @static
		 */
		MovePageWarnings.init = function() {
			$.when(
				mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.util']),
				formReady()
			).then(function() { // Load modules and the DOM, then

				var prefixLabel = document.querySelector('#wpNewTitleNs span.oo-ui-labelElement-label');

				/** @type {HTMLInputElement?} */
				var titleInput = document.querySelector('#wpNewTitleMain > input');

				var $submitButton = $('button[name="wpMove"]').eq(0).parent('span');

				// Run the script if all the above are defined
				if (prefixLabel && titleInput && $submitButton.length) {
					new MovePageWarnings(prefixLabel, titleInput, $submitButton);
				}

			}).catch(console.error);
		};

		/**
		 * The movepage form is created by OOUI dynamically, so it's not enough to just wait for document ready.
		 * This function ensures that the form is ready in the document.
		 *
		 * See `[[Special:Permalink/98980431]]` for the older 1.0.X version of this function written with MutationObserver,
		 * which turned out to not work well if the page has been opened on a different tab (see also the bottom of this
		 * script for a handler of this situation).
		 */
		function formReady() {
			var def = $.Deferred();

			/** @returns {boolean} */
			var elementsReady = function() {
				return !!(
					document.querySelector('#wpNewTitleNs') &&
					document.querySelector('#wpNewTitleMain') &&
					document.querySelector('button[name="wpMove"]')
				);
			};

			$(function() { // When the document is ready

				// Check the ready state of form elements every 0.5 seconds (up to 10 times)
				var iterations = 0;
				var interval = setInterval(function() {
					if ((++iterations) > 10) {
						// If we have already done 10 iterations, reject the procedure
						clearInterval(interval);
						def.reject(new Error('[mpw] The form never got ready'));
					} else if (elementsReady()) {
						// If the form elements are ready, resolve the procedure
						clearInterval(interval);
						def.resolve();
					}
					// <= Proceed to the next interval
				}, 500);

			});

			return def.promise();
		}

		Object.defineProperty(MovePageWarnings.prototype, 'moveTalk', {
			/**
			 * Return the check state of the `Move associated talk page` box.
			 * @returns {boolean}
			 */
			get: function() {
				return this.moveTalkBox && this.moveTalkBox.checked || false;
			}
		});

		Object.defineProperty(MovePageWarnings.prototype, 'length', {
			/**
			 * Return the number of warnings.
			 * @returns {number}
			 */
			get: function() {
				return this.$warningList.children('li').length;
			}
		});

		/**
		 * Add a \<style> for MovePageWarnings.
		 * @static
		 */
		MovePageWarnings.addStyleTag = function() {
			var style = document.createElement('style');
			style.textContent =
				'.mpw-seewarning::after {' +
					'display: inline-block;' +
					'content: "※ 下記の警告も確認してください";' +
					'color: red;' +
					'font-family: inherit;' +
					'font-weight: bold;' +
					'margin-left: 1em;' +
					'padding-top: 5px;' +
				'}' +
				'.mpw-logline-hidden {' +
					'text-decoration: line-through;' +
					'color: #72777d;' +
					'font-style: italic;' +
				'}';
			document.head.appendChild(style);
		};

		/**
		 * Toggle the visibility of warnings.
		 * @param {boolean} show
		 */
		MovePageWarnings.prototype.toggle = function(show) {
			this.$submitButton.toggleClass('mpw-seewarning', show);
			this.$warning.toggle(show);
		};

		/**
		 * @param {string} logline
		 */
		function log(logline) {
			console.log('[mpw]', logline);
		}

		/**
		 * Update warnings. This is to be called when either the prefix or the title has been changed.
		 * @param {boolean} moveTalkChanged
		 * @returns {JQueryPromise<void>}
		 */
		MovePageWarnings.prototype.updateWarnings = function(moveTalkChanged) {

			// Pick up the page name to which to move the current page
			var prefix = this.prefix && this.prefix + ':';
			var title = this.titleInput.value.replace(rUnicodeBidi, '');
			var pagename = prefix + title;
			var hasPrefixInTitle = rAliases.test(title);
			var hasDuplicatePrefixes = !!prefix && hasPrefixInTitle;
			var Title = mw.Title.newFromText(pagename);
			pagename = Title ? Title.getPrefixedText() : pagename.replace(/_/g, ' ');
			log('Move destination: ' + pagename);

			// Compare with the last-checked pagename
			var isSamePagename = pagename === this.lastPagename;
			this.lastPagename = pagename;
			var isSameAsTarget = pagename === this.target;

			// Synchronous checks for possible warnings
			if (isSamePagename && !moveTalkChanged) {
				log('Exited for the reason of "same pagename".');
				return $.Deferred().resolve(void 0);
			} else if (!pagename || isSameAsTarget && !hasPrefixInTitle && !hasDuplicatePrefixes) {
				log('Exited for the reason of "no pagename" or "same as target pagename".');
				this.api.abort();
				this.clearWarnings();
				return $.Deferred().resolve(void 0);
			} else if (isSameAsTarget || !Title) {
				log('Exited for the reason of "invalid pagename".');
				this.api.abort();
				this.setWarnings({
					invalidPagename: !Title ? [pagename] : null,
					misplacedPrefix: hasPrefixInTitle ? [] : null,
					duplicatePrefixes: hasDuplicatePrefixes ? [pagename] : null
				});
				return $.Deferred().resolve(void 0);
			}

			// Asynchronous checks for possible warnings
			this.api.abort();
			var _this = this;
			var talkTitle = this.moveTalk && Title && !Title.isTalkPage() && Title.getTalkPage() || void 0;
			var talkPagename = talkTitle && talkTitle.getPrefixedText();
			return this.getVerificationFunc(pagename, talkPagename).then(function(is) {

				if (!is('main', 'verifiable')) {
					log('Exited for the reason of "info is null".');
					_this.clearWarnings();
				} else {

					log('Generated warnings.');
					_this.setWarnings({
						invalidPagename: !is('main', 'valid') ? [pagename] : null,
						misplacedPrefix: hasPrefixInTitle ? [] : null,
						duplicatePrefixes: hasDuplicatePrefixes ? [pagename] : null,
						overwriteRedirect: is('main', 'overwritable') ? [pagename] : null,
						overwriteTalkRedirect: talkPagename && is('talk', 'overwritable') ? [talkPagename] : null,
						talkPageExists: talkPagename && is('talk', 'verifiable') && !is('talk', 'missing') && !is('talk', 'overwritable') ? [talkPagename] : null,
						deleteToMove: !(is('main', 'missing') || is('main', 'overwritable')) && _this.candelete ? [pagename] : null,
						cantDelete: !(is('main', 'missing') || is('main', 'overwritable')) && !_this.candelete ? [pagename] : null
					});

					var prot, pwCnt = 0;
					if ((prot = is('main', 'protected'))) {
						pwCnt += _this.setProtectionWarning(prot);
					}
					if (talkPagename && (prot = is('talk', 'protected'))) {
						pwCnt += _this.setProtectionWarning(prot);
					}
					if (pwCnt) {
						_this.searchRedlinks();
					}

				}

				if (_this.length) {
					// If new warnings have been generated, trigger the wikipage.content hook to run any script
					// that watches the hook for updates in the page content (this will activate e.g. nav_popups
					// on links in the warnings)
					mw.hook('wikipage.content').fire(_this.$warningList);
				}

			});

		};

		/**
		 * Generate a wrapper for a protection warning.
		 * @param {string} str
		 * @returns {string}
		 */
		var protectionWarningWrapper = function(str) {
			return '<b>移動先のページは保護されています</b>。<ul><li>' + str + '</li></ul>';
		};

		/**
		 * Warning templates.
		 * @static
		 */
		MovePageWarnings.template = {
			/** `$1`: page name */
			invalidPagename: '「$1」は[[Help:ページ名#特殊文字|不正なページ名]]です。',
			misplacedPrefix: 'ページ名指定用テキストボックスの値に[[H:NS#詳細|名前空間]]接頭辞が含まれています。(名前空間の指定にはドロップダウンを' +
				'使用してください。)',
			/** `$1`: page name */
			duplicatePrefixes: '「[[$1]]」には重複した[[H:NS#詳細|名前空間]]接頭辞が含まれています。',
			/** `$1`: page name */
			overwriteRedirect: 'リダイレクトの「[[$1]]」を上書きして移動します。',
			/** `$1`: page name */
			overwriteTalkRedirect: '付随移動により、ノートページリダイレクトの「[[$1]]」を上書きして移動します。',
			/** `$1`: page name */
			talkPageExists: '「[[$1]]」が存在するため、<b>ノートページは付随移動されません</b>。',
			/** `$1`: page name */
			deleteToMove: '「[[$1]]」への移動を行うためにはページの削除が必要です。',
			/** `$1`: page name */
			cantDelete: '「[[$1]]」への移動は削除権限を要するため、<b>あなたは移動できません</b>。記事としての履歴がない (またはあっても' +
				'即時削除対象となる) 場合は[[Wikipedia:移動依頼|移動依頼]]を、そうでない場合は[[Wikipedia:削除の方針#C|ケースC]]の' +
				'[[Wikipedia:削除依頼|削除依頼]]を利用してください。',
			/** `$1`: logid, `$2`: timestamp, `$3`: user, `$4`: target, `$5`: levels, `$6`: parsedcomment */
			'protect/protect': protectionWarningWrapper(
				'[[Special:Redirect/logid/$1|$2]] <span class="mpw-logline-user">[[User:$3|$3]] ([[User_talk:$3|会話]] | ' +
				'[[Special:Contribs/$3|投稿記録]])</span><span class="mpw-logline-connective"></span><span class="mpw-logline-action">' +
				'[[$4]] を保護しました $5</span> <span class="mpw-logline-comment">$6</span>'
			),
			/** `$1`: logid, `$2`: timestamp, `$3`: user, `$4`: target, `$5`: levels, `$6`: parsedcomment */
			'protect/modify': protectionWarningWrapper(
				'[[Special:Redirect/logid/$1|$2]] <span class="mpw-logline-user">[[User:$3|$3]] ([[User_talk:$3|会話]] | ' +
				'[[Special:Contribs/$3|投稿記録]])</span><span class="mpw-logline-connective"></span><span class="mpw-logline-action">' +
				'[[$4]] の保護設定を変更しました $5</span> <span class="mpw-logline-comment">$6</span>'
			),
			/** `$1`: logid, `$2`: timestamp, `$3`: user, `$4`: target, `$5`: moved_from, `$6`: parsedcomment */
			'protect/move_prot': protectionWarningWrapper(
				'[[Special:Redirect/logid/$1|$2]] <span class="mpw-logline-user">[[User:$3|$3]] ([[User_talk:$3|会話]] | ' +
				'[[Special:Contribs/$3|投稿記録]])</span><span class="mpw-logline-connective-move"></span><span class="mpw-logline-action">' +
				'保護設定を [[$5]] から [[$4]] に移動しました</span> <span class="mpw-logline-comment">$6</span>'
			)
		};

		/**
		 * Set warnings.
		 *
		 * @param {Partial<Record<keyof MovePageWarnings.template, string[]?>>} warningMap
		 * The values should be an array of variables for `mw.format`, or `null` if they shouldn't be converted to warnings.
		 * @returns {number} The number of warnings generated.
		 */
		MovePageWarnings.prototype.setWarnings = function(warningMap) {

			// Erase old warnings
			this.$warningList.empty();

			// Loop each object key and set up a warning if the corresponding value is an array
			for (var key in warningMap) {
				var variables = warningMap[key];
				if (variables) {
					var li = document.createElement('li');
					// @ts-ignore
					li.innerHTML = createWarning(key, variables);
					this.$warningList.append(li);
				}
			}

			// Show/hide the warning wrapper depending on whether there's at least one warning
			var cnt = this.length;
			this.toggle(!!cnt);
			return cnt;

		};

		/**
		 * Clear all warnings.
		 * @returns {number} The number of warnings generated (always 0).
		 */
		MovePageWarnings.prototype.clearWarnings = function() {
			this.$warningList.empty();
			this.toggle(false);
			return 0;
		};

		/**
		 * Create a warning message as a raw HTML by parsing [[links]] and $-variables.
		 *
		 * @param {keyof MovePageWarnings.template} key
		 * @param {string[]} variables
		 * @param {string} [template] Use this template instead of what can be obtained by the key
		 * @returns {string}
		 */
		function createWarning(key, variables, template) {

			// Get template and replace variables
			var def = template || MovePageWarnings.template[key];
			def = mw.format.apply(mw, [def].concat(variables)); // Same as "mw.format(def, $1, $2, ...)"
			var transformed = def;

			// Parse [[links]]
			var rLink = /\[\[([^|\]]+)\|?([^\]]*)\]\]/g; // Matches [[page|display]] or [[page]]
			var m;
			while ((m = rLink.exec(def))) {
				// Replace the [[link]] with <a>
				transformed = transformed.replace(m[0], createLink(m[1], m[2]));
			}

			return transformed;

		}

		/**
		 * Create an anchor tag as a raw HTML.
		 *
		 * @param {string} page
		 * @param {string} [display]
		 * @returns {string}
		 */
		function createLink(page, display) {
			return '<a href="' + mw.util.getUrl(page, {redirect: 'no'}) + '">' + (display || page) + '</a>';
		}

		/**
		 * Set a protection warning.
		 *
		 * @param {TitleInfoProtection} info
		 * @returns {number} The number of warnings generated.
		 */
		MovePageWarnings.prototype.setProtectionWarning = function(info) {

			if (!info.action) return 0;

			// Get template
			var key = 'protect/' + info.action;
			/** @type {string} */
			var template = MovePageWarnings.template[key];
			if (!template) return 0;

			// Handle hidden parts in the logline, if any
			var $logline = $('<div>').prop('innerHTML', template); // Temporarily convert the string to a JQuery element
			var userHidden = info.user === null;
			var actionHidden = info.actionhidden;
			var commentHidden = info.parsedcomment === null;
			if (!userHidden && !actionHidden) {
				$logline.find('.mpw-logline-connective').text(' が ');
				$logline.find('.mpw-logline-connective-move').text(' が');
			} else {
				$logline.find('.mpw-logline-connective, .mpw-logline-connective-move').text(' ');
			}
			if (userHidden) {
				$logline.find('.mpw-logline-user').addClass('mpw-logline-hidden').prop('innerHTML', '(利用者名は除去されています)');
			}
			if (actionHidden) {
				$logline.find('.mpw-logline-action').addClass('mpw-logline-hidden').prop('innerHTML', '(ログの詳細は除去されています)');
			}
			if (commentHidden) {
				$logline.find('.mpw-logline-comment').addClass('mpw-logline-hidden').prop('innerHTML', '(要約は除去されています)');
			}
			template = $logline.prop('innerHTML'); // Convert back to a string

			// Get variables to mw.format
			var variables = [
				String(info.logid),
				info.timestamp,
				info.user || void 0,
				info.target,
				info.action === 'move_prot' ? info.moved_from : translateLevels(info.levels),
				info.parsedcomment && ('(' + info.parsedcomment + ')') || ''
			];

			// Create warning
			var li = document.createElement('li');
			// @ts-ignore
			li.innerHTML = createWarning(key, variables, template);
			this.$warningList.append(li);
			this.toggle(true);
			return 1;

		};

		/**
		 * Turn blue links into red ones if any anchor in the warnings is linked to a non-existing page.
		 *
		 * This is to be called after `setProtectionWarning`.
		 *
		 * @returns {JQueryPromise<void>}
		 */
		MovePageWarnings.prototype.searchRedlinks = function() {

			/**
			 * @typedef {Record<string, HTMLAnchorElement[]>} AnchorMap
			 * Keyed by a page title and valued by anchors
			 */
			/** @type {AnchorMap} */
			var anchors = Array.prototype.reduce.call( // Collect anchors by pagename and create a mapping object
				this.$warningList.find('a'),
				/**
				 * @param {AnchorMap} acc
				 * @param {HTMLAnchorElement} a
				 */
				function(acc, a) {
					var title = mw.util.getParamValue('title', a.href);
					if (title) {
						if (!acc[title]) acc[title] = [];
						acc[title].push(a);
					}
					return acc;
				},
				Object.create(null)
			);
			if ($.isEmptyObject(anchors)) return $.Deferred().resolve(void 0);

			// Check page existence
			var pagenames = Object.keys(anchors);
			return this.getExistenceFunc(pagenames).then(function(exists) {
				pagenames.forEach(function(p) {
					if (anchors[p] && !exists(p)) { // If the page doesn't exist
						anchors[p].forEach(function(a) {
							a.classList.add('new'); // Add class that applies the redlink CSS
						});
					}
				});
			});

		};

		/**
		 * Translate e.g. "[edit=autoconfirmed] (無期限)[move=autoconfirmed] (無期限)".
		 * @param {string} [levels]
		 */
		function translateLevels(levels) {

			if (levels === void 0) return levels;

			var translations = {
				create: '作成',
				edit: '編集',
				move: '移動',
				upload: 'アップロード',
				autoconfirmed: '自動承認された利用者のみ許可',
				extendedconfirmed: '拡張承認された利用者と管理者に許可',
				sysop: '管理者のみ許可'
			};
			var rLevels = /\[([^=]+)=([^\]]+)\]/g;
			var m;
			var ret = levels;
			while ((m = rLevels.exec(levels))) {
				var line = m[0]
					.replace(m[1], translations[m[1]] || m[1])
					.replace(m[2], translations[m[2]] || m[2]);
				ret = ret.replace(m[0], line);
			}

			return ret;

		}

		/**
		 * Get a function that verifies the properties of moving destination(s).
		 * @param {string} mainPagename
		 * @param {string} [talkPagename]
		 */
		MovePageWarnings.prototype.getVerificationFunc = function(mainPagename, talkPagename) {
			return $.when(
				this.queryTitleInfo(mainPagename),
				this.queryTitleInfo(talkPagename),
				this.getRedirectMap(talkPagename ? [mainPagename, talkPagename] : [mainPagename])
			).then(function(mInfo, tInfo, rMap) {
				/**
				 * @overload
				 * @param {"main"|"talk"} target
				 * @param {"verifiable"|"valid"|"missing"|"overwritable"} type
				 * @returns {boolean}
				 */
				/**
				 * @overload
				 * @param {"main"|"talk"} target
				 * @param {"protected"} type
				 * @returns {TitleInfoProtection=}
				 */
				/**
				 * @param {"main"|"talk"} target
				 * @param {"verifiable"|"valid"|"missing"|"overwritable"|"protected"} type
				 * @returns {boolean|TitleInfoProtection=}
				 */
				var verify = function(target, type) {
					var info = target === 'main' ? mInfo : tInfo;
					switch (type) {
						case 'verifiable':
							return !!info;
						case "valid":
							return !(info && info.invalid);
						case "missing":
							return !!(info && info.missing);
						case "overwritable":
							return !!(info && info.single && info.redirect && rMap[info.title || ''] === (function() {
								if (target === 'main') {
									return moveFrom;
								} else {
									var tp = new mw.Title(moveFrom).getTalkPage();
									return tp && tp.getPrefixedText();
								}
							})());
						case "protected":
							return info && info.protection || void 0;
						default:
							throw new Error();
					}
				};
				return verify;
			});
		};

		/**
		 * @typedef ApiResponse
		 * @type {{
		 * 	query?: {
		 * 		normalized?: ApiResponseNormalized[];
		 * 		redirects?: {
		 * 			from: string;
		 * 			to: string;
		 * 		}[];
		 * 		pages?: {
		 * 			ns: number;
		 * 			title: string;
		 * 			missing?: boolean;
		 * 			known?: boolean;
		 * 			redirect?: boolean;
		 * 			invalid?: boolean;
		 * 			invalidreason?: string;
		 * 			protection?: {
		 * 				type: string;
		 * 				level: string;
		 * 				expiry: string;
		 * 			}[];
		 * 			revisions?: {
		 * 				revid: number;
		 * 				parentid: number;
		 * 			}[];
		 * 		}[];
		 * 		logevents?: {
		 * 			logid: number;
		 *			title: string;
		 * 			params: {
		 *				description?: string;
		 *				cascade?: boolean;
		 *				details?:  ApiResponseLogeventsParamsDetails[];
		 *				oldtitle_ns?: number;
		 *				oldtitle_title?: string;
		 *			};
		 *			type: "protect";
		 *			actionhidden?: boolean;
		 *			action?: "protect"|"modify"|"move_prot";
		 *			userhidden?: boolean;
		 *			user?: string;
		 *			timestamp: string;
		 *			commenthidden?: boolean;
		 *			parsedcomment?: string;
		 * 		}[];
		 * 	};
		 * }}
		 */
		/**
		 * @typedef ApiResponseNormalized
		 * @type {{
		 *	fromencoded: boolean;
		 *	from: string;
		 *	to: string;
		 * }}
		 */
		/**
		 * @typedef ApiResponseLogeventsParamsDetails
		 * @type {{
		 *	type: string;
		 *	level: string;
		 *	expiry: string;
		 *	cascade: boolean;
		 * }}
		 */
		/**
		 * The object returned by `MovePageWarnings.queryTitleInfo`.
		 * @typedef {object} TitleInfo
		 * @property {string} [title]
		 * @property {boolean} [missing]
		 * @property {boolean} [redirect]
		 * @property {boolean} [invalid]
		 * @property {boolean} [protected]
		 * @property {boolean} [single]
		 * @property {TitleInfoProtection} [protection]
		 */
		/**
		 * @typedef {object} TitleInfoProtection
		 * @property {("protect"|"modify"|"move_prot")?} action `null` if hidden
		 * @property {boolean} actionhidden
		 * @property {number} logid
		 * @property {string} timestamp
		 * @property {string?} user `null` if hidden
		 * @property {string} target
		 * @property {string} [levels]
		 * @property {string} [moved_from]
		 * @property {string?} parsedcomment `null` if hidden
		 */

		/**
		 * Get information about a move destination.
		 *
		 * @param {string=} title
		 * @returns {JQueryPromise<TitleInfo?>}
		 */
		MovePageWarnings.prototype.queryTitleInfo = function(title) {
			if (!title) {
				return $.Deferred().resolve(null);
			}
			return this.api.get({
				action: 'query',
				titles: title,
				prop: 'info|revisions',
				inprop: 'protection',
				rvprop: 'ids',
				rvlimit: 2,
				list: 'logevents',
				leprop: 'ids|title|type|user|timestamp|parsedcomment|details',
				letype: 'protect',
				letitle: title,
				lelimit: 'max',
				formatversion: '2'
			}).then(/** @param {ApiResponse} res */ function(res) {

				/** @type {TitleInfo} */
				var ret = {};

				var resPg = res && res.query && res.query.pages && res.query.pages[0];
				if (resPg) {
					$.extend(ret, {
						title: resPg.title,
						missing: !!(resPg.missing && !resPg.known),
						redirect: !!resPg.redirect,
						invalid: !!resPg.invalid,
						protected: Array.isArray(resPg.protection) && !!resPg.protection.length,
						single: Array.isArray(resPg.revisions) && resPg.revisions.length === 1
					});
				}
				var resLgev = res && res.query && res.query.logevents;
				if (resLgev && ret.protected) {
					for (var i = 0; i < resLgev.length; i++) {
						var obj = resLgev[i];
						if (
							['protect', 'modify', 'move_prot'].indexOf(obj.action || '') !== -1 &&
							// The first log entry might not be the one assocaited with the current protection settings,
							// if the log body has been deleted and the script user doesn't have the "deletelogentry" user right
							isProtected(obj.params.details)
						) {
							ret.protection = {
								action: obj.action || null,
								actionhidden: !!obj.actionhidden, // If this is true and the user doesn't have "deletelogentry", the entire log is gone
								logid: obj.logid,
								timestamp: obj.timestamp.replace(/Z$/, ''),
								user: !obj.userhidden && obj.user || null,
								target: obj.title,
								levels: obj.params.description && obj.params.description
									.replace(rUnicodeBidi, '') // Remove unicode bidirectional markers
									.replace(/([^ ])\[/g, '$1 ['), // Ensure that there's a space before every "["
								moved_from: obj.params.oldtitle_title,
								parsedcomment: !obj.commenthidden && obj.parsedcomment || null
							};
							break;
						}
					}
				}
				return ret;
			}).catch(function(_, err) {
				if (err && err['exception'] !== 'abort') {
					console.log(err);
				}
				return null;
			});
		};

		/**
		 * Look at the details array of a `list=logevents&letype=protect` response and check if the relevant page is currently protected.
		 *
		 * @param {ApiResponseLogeventsParamsDetails[]} [details]
		 * @returns {boolean} Always `true` if `undefined` is passed
		 */
		function isProtected(details) {
			var d = new Date();
			if (!Array.isArray(details)) {
				return true;
			} else {
				for (var i = 0; i < details.length; i++) {
					var obj = details[i];
					if (!obj.expiry) {
						continue;
					} else if (/^in/.test(obj.expiry)) {
						return true;
					} else {
						return d < new Date(obj.expiry);
					}
				}
				return false;
			}
		}

		/**
		 * Get a function to normalize pagenames (as in API responses).
		 *
		 * @param {ApiResponseNormalized[]} [normalized] response.query.normalized
		 * @returns {(page: string) => string} Function that takes a pagename and formats it
		 */
		function normalizerFactory(normalized) {
			var normalizerMap = (normalized || []).reduce(/** @param {Record<string, string>} acc */ function(acc, obj) {
				acc[obj.from] = obj.to; // Keyed by non-canonical pagenames and valued by canonical, normalized ones
				return acc;
			}, Object.create(null));
			return /** @param {string} page */ function(page) {
				return normalizerMap[page] || page; // Get the normalized pagesname, falling back to the input pagename
			};
		}

		/**
		 * Get mappings from redirecting pages to redirected pages.
		 * @param {string[]} pagenames
		 * @returns {JQueryPromise<Record<string, string>>} Object keyed by redirecting pages (where the titles are normalized)
		 * and valued by redirected pages. 
		 */
		MovePageWarnings.prototype.getRedirectMap = function(pagenames) {
			return this.api.get({
				action: 'query',
				titles: pagenames.join('|'),
				redirects: true,
				formatversion: '2'
			}).then(/** @param {ApiResponse} res */ function(res) {
				var ret = Object.create(null);
				if (res && res.query) {
					(res.query.redirects || []).forEach(function(obj) {
						ret[obj.from] = obj.to;
					});
				}
				return ret;
			}).catch(function(_, err) {
				if (err && err['exception'] !== 'abort') {
					console.log(err);
				}
				return Object.create(null);
			});
		};

		/**
		 * Get a function from a pagename to its existence boolean.
		 *
		 * @param {string[]} [pagenames]
		 * @returns {JQueryPromise<(page: string) => boolean>}
		 */
		MovePageWarnings.prototype.getExistenceFunc = function(pagenames) {
			if (pagenames === void 0 || !pagenames.length) {
				// eslint-disable-next-line @typescript-eslint/no-unused-vars
				return $.Deferred().resolve(/** @param {string} page */ function(page) { return false; });
			}
			/** @typedef {Record<string, boolean>} ExistenceMap */
			return this.api.get({
				action: 'query',
				titles: pagenames,
				formatversion: '2'
			}).then(/** @param {ApiResponse} res */ function(res) {
				var normalize = normalizerFactory(res && res.query && res.query.normalized);
				var fPagenames = pagenames.map(function(p) {
					return normalize(p);
				});
				return (res && res.query && res.query.pages || []).reduce(/** @param {ExistenceMap} acc */ function(acc, obj) {
					var index = fPagenames.indexOf(obj.title);
					if (index !== -1) {
						acc[pagenames[index]] = !(obj.missing && !obj.known);
					}
					return acc;
				}, Object.create(null));
			}).catch(function(_, err) {
				if (err && err['exception'] !== 'abort') {
					console.log(err);
				}
				return Object.create(null);
			}).then(/** @param {ExistenceMap} existenceMap */ function(existenceMap) {
				/** @param {string} page */
				return function(page) {
					return !!existenceMap[page];
				};
			});
		};

		return MovePageWarnings;

	})();

	// Entry point
	if (document.hidden) {
		// If Special:Movepage is opened on an inactive tab, wait until the tab gets active
		var vc = 'visibilitychange';
		var init = function() {
			if (!document.hidden) {
				document.removeEventListener(vc, init);
				MovePageWarnings.init();
			}
		};
		document.addEventListener(vc, init);
	} else {
		MovePageWarnings.init();
	}

})();
//</nowiki>