MediaWiki:Gadget-iwrm.js (MediaWiki&Gadget-iwrmQjs)

Перейти к навигации Перейти к поиску
JS-код ниже относится к гаджету «Интерфейс для замены прямых интервики-ссылок в полуавтоматическом режиме для проекта Check Wikipedia» (править описание). Связанный CSS-файл: MediaWiki:Gadget-iwrm.css. Его использует около 200 учётных записей.

После сохранения или недавних изменений очистите кэш браузера.

/* <nowiki>
 * IWRM.js
 * See [[Проект:Check Wikipedia/Замена прямых интервики-ссылок]]
 * Local and global variables are lowerCamelCase
 * Selectors and DOM nodes are CamelCase
 * Local variables start with _
 */
( function() {
	if ( !window.IWRM ) window.IWRM = {};

	/*
	 * Global settings and variables
	 */
	IWRM.prefs = {
		name: 'IWRM',
		tag: 'iwrm',
		hash: '#/iwrm/',
		loaded: 'is-loaded',

		limit: 50,

		summary: '[[ПРО:CW|CheckWiki:]] замена прямых интервики-ссылок'
	}

	// API requests
	IWRM.api = {};

	// Current article data
	IWRM.data = {
		index: 0,
		title: '',
		text: '',
		modifiedText: '',
		changeCount: 0
	}

	// Current article list
	IWRM.list = [];

	// Current search offset
	IWRM.offset = 0;

	// Storage for referenced UI elements
	IWRM.ui = {};

	/*
	 * Local settings and variables
	 */
	var _c = mw.config.get( [
		'wgAction',
		'wgCanonicalSpecialPageName',
		'wgCommentCodePointLimit',
		'wgContentLanguage',
		'wgDBname',
		'wgNamespaceNumber',
		'wgPageName',
		'wgTitle',
		'wgUserGroups',
		'wgUserName',
	] );

	// Special:BlankPage, category or individual page
	var _isSiteWide = true;
	var _isCategory = _c.wgNamespaceNumber === 14;
	if ( _c.wgCanonicalSpecialPageName !== 'Blankpage' ) {
		_isSiteWide = false;
	}

	// Is user allowed to make edits
	var _isAllowed = (
		_c.wgUserName &&
		(
			_c.wgUserGroups.includes( 'bot' ) ||
			_c.wgUserGroups.includes( 'autoreview' ) ||
			_c.wgUserGroups.includes( 'editor' ) ||
			_c.wgUserGroups.includes( 'sysop' )
		)
	);

	// API handler container
	var _api = null;

	// Wikidata API handler container
	var _wikidataApi = null;

	// Counter for successful savings
	var _counter = 0;

	// Active notification container for closing
	var _notification;

	// Dependencies
	var _deps = [
		'ext.gadget.wikificator',
		'mediawiki.action.view.postEdit',
		'mediawiki.api',
		'mediawiki.confirmCloseWindow',
		'mediawiki.ForeignApi',
		'mediawiki.diff.styles',
		'mediawiki.notification',
		'mediawiki.util',
		'mediawiki.widgets.visibleLengthLimit',
		'oojs',
		'oojs-ui'
	];

	// Normalised prefixes
	var _normalisedPrefixes = {
		'w': 'en',

		'be-x-old': 'be-tarask',
		'cz': 'cs',
		'jp': 'ja',
		'nan': 'zh-min-nan',
		'nb': 'no',
		'yue': 'zh-yue',
		'zh-tw': 'zh'
	};

	var _normalisedPrefixKeys = Object.keys( _normalisedPrefixes );

	// Skipped interwiki links
	var _otherProjects = [
		'b', 'wikibooks',
		'c', 'commons',
		'd', 'wikidata',
		'm', 'meta',
		'mw',
		'n', 'wikinews',
		'q', 'wikiquote',
		's', 'wikisource',
		'species', 'wikispecies',
		'v', 'wikiversity',
		'voy', 'wikivoyage',
		'wikt', 'wiktionary',
		'wmf', 'wikimedia',

		'category', 'file', 'image', 'media', 'wikipedia',

		'betawiki', 'doi', 'translatewiki'
	];

	// Expression for interwiki links: [[$1:$2]]
	var _regularExp = /\[{2}:? *([a-z-]+) *: *([^\[\]\|\n]+)(?:\|([^\|\n]*?))?\]{2}/gi;

	// Expression for interwiki links in language: [[$1]] ({{lang-$2|$3}}$4
	var _regularExpLangs = /\[{2}([^\[\]\n]+?)\]{2} \(\{\{[Ll]ang-([a-zA-Z-]+)\|(\[{2}:?[^\]]*?\]{2})\}\}(\)*)/g;

	// Expression for prefixes: [[:$1:]]
	var _regularPrefix = / *:? *([a-z-]+) *: */gi;
	
	// Expression for templates: {{iw}}, {{нп5}}, {{не переведено 5}}
	var _regularTmpl = /\{\{(?:subst:|подст:|safesubst:)?(?:iw|нп\d+|не переведено \d+)\|(.*?)[\|\}]/gi;

	// Expression for bad text
	var _regularBadText = /[\[\]\{\}<>]/;

	// Search auto update limit
	var _searchAutoUpdate = 10;

	// Search API high limit
	var _searchHighLimit = 500;

	// Search request for interwiki links
	var _searchRequest = 'insource:/\\[{2}:[a-z-]{2,}:/';

	// Default link without/with {{lang}} syntax
	var _link = '[[$1$2]]';
	var _linkIsLang = '[[$1$2]] ({{lang-$3|$4}}$5';
	var _linkTextOnly = '$2';

	// Default template without/with {{lang}} syntax
	var _tmpl = '{{iw||$3|$1|$2}}';
	var _tmplIsLang = '{{iw|$4|$3|$1|$2}} ({{lang-$5|$6}}$7';

	// Default link and template syntax
	var _linkPipe = '|';
	var _linkEnd = ']]';
	var _tmplStart = '{{iw||';

	// Talk namespace prefix
	var _talkNs = 'Обсуждение:';

	// UI functions
	var _ui = {};
	_ui.fn = {};
	IWRM._ui = _ui;

	// Most used selectors
	var _ContentText = $( '#mw-content-text' );
	var _FirstHeading = $( '#firstHeading' );
	var _SiteSub = $( '#siteSub' );
	var _ContentSub = $( '#contentSub' );
	var _Indicators = $( '.mw-indicators' );

	// Locale
	var _locale = {
		title: 'Замена $1прямых интервики-ссылок',
		titleOne: 'Замена $1прямой интервики-ссылки',
		errors: {
			title: 'Скрипт мог перестать работать',
			text: 'Произошла ошибка. ',
			unidentified: 'Запрос $1 не был выполнен по неизвестной причине.',

			notAllowed: 'Чтобы не распатрулировать статьи, скрипт можно использовать только автопатрулируемым, патрулирующим и администраторам.',

			noArticle: 'Страница «$1» не найдена. Вероятно, до неё добрались удалисты, переименисты или коммунисты.',
			noLinks: 'Не обнаружено прямых интервики-ссылок. Пропустите страницу.',
			unchangedCode: 'Вы не заполнили первый параметр в одном из шаблонов или не заполнили текст ссылки. Изменения не были записаны, проверьте все поля ввода.',

			saving: 'Страница «$1» сохраняется…',
			altOnlyLink: 'Это единственная ссылка в данной статье — к другим перейти невозможно.',
			altNoNext: 'Это последняя ссылка в данной статье — можно перейти только к предыдущей.',

			noNetwork: 'Запрос $1 не дошёл из-за неполадок с сетью. Проверьте подключение к Интернету.',
			noText: 'В запросе $1 не был передан итоговый текст.',
			noTitle: 'В запросе $1 не был передан заголовок необходимой статьи.',

			articleExists: '$1: $2страница уже существует$3.',
			badInterwiki: '<strong>API Викиданных не может найти раздел Википедии ([[$1:…]]) для одной из ссылок.</strong> Она не будет отредактирована.'
		},
		requests: {
			init: 'по загрузке модулей',

			getFullText: 'для получения текста статьи',
			getRateLimits: 'для выяснения максимального количества доступных вам данных',
			getSearchData: 'для получения списка статей для обработки',
			loadDiff: 'для получения разницы версий',
			submitChanges: 'для записи изменений'
		},

		loading: 'Поиск в $1 займёт некоторое время, но это того стоит.',
		loadingTitle: 'Загружается…',

		loadMore: 'Загрузить ещё',
		nearbyPage: {
			previous: 'Предыдущая',
			next: 'Следующая'
		},
		randomPage: 'Случайная статья',

		notice: {
			moveUpDown: 'Перемещаться по полям можно через [Alt+J] (предыдущее) / [Alt+K] (следующее)',

			editSection: 'Править статью вручную (откроется в новой вкладке)',
			viewArticle: 'Перейти в статью (откроется в новой вкладке)',
			viewTalk: 'Перейти в обсуждение (откроется в новой вкладке)',

			interwiki: 'Интервики-ссылка (откроется в новой вкладке)',
			thiswiki: 'Статья в этом разделе (откроется в новой вкладке)',
			wikidata: 'Элемент Викиданных (откроется в новой вкладке)',

			thiswikiArticle: 'есть статья',
			
			clearInput: 'очистить текст',
			clearInputRestore: 'вернуть очищенное',
		},
		editParagraph: 'Править весь абзац',
		editSection: 'править вручную',
		viewArticle: 'просмотр статьи',

		mainTab: 'Статья',
		talkTab: 'Обсуждение',

		summary: 'Описание изменений',
		warrant: 'Вы сами отвечаете за правки, сделанные с помощью скрипта. <br><small>Удалите всё содержимое нужного поля ввода, чтобы не обрабатывать связанную с ним ссылку.</small>',
		submit: 'Записать страницу',
		clearAll: 'Очистить текст везде',
		clearAllRestore: 'Вернуть очищенное везде',
		skipPage: 'Пропустить страницу'
	}

	/*
	 * API requests
	 */

	// Check sitelinks
	IWRM.api.checkSitelinks = ( lang, title ) => {
		function getDBname( lang ) {
			if ( lang === 'be-tarask' ) {
				return 'be_x_old' + 'wiki';
			}

			return lang.split( '-' ).join( '_' ) + 'wiki';
		}

		return _wikidataApi.get( {
			action: 'wbgetentities',
			sites: getDBname( lang ),
			sitefilter: _c.wgDBname,
			titles: title,
			props: 'sitelinks',
			normalize: true,
			formatversion: 2
		} );
	}

	// Get full article text
	IWRM.api.getFullText = ( title ) => {
		if ( !title ) {
			_ui.fn.notify( 'script', null, {
				replace: _locale.requests.getFullText,
				text: _locale.errors.noTitle
			} );
			return false;
		}

		// Success
		function resolvedWithSuccess( data ) {
			var revision = OO.getProp( data, 'query', 'pages', 0, 'revisions', 0 );

			if ( revision ) {
				IWRM.data.text = revision.content;

				// Modify the text prematurely to show the changes
				IWRM.data.modifiedText = replaceLinks( revision.content, ( obj, text ) => {
					var modified = getTemplate( obj );

					return text.replace( obj.fullText, modified );
				} );
				return;
			}

			var isMissing = OO.getProp( data, 'query', 'pages', 0, 'missing' );
			if ( isMissing ) {
				_ui.fn.notify( 'missingtitle', null, { replace: title } );
			}
			return;
		}

		// Failure
		function resolvedWithFailure( error, data ) {
			_ui.fn.notify( error, data, { replace: _locale.requests.getFullText } );
		}

		// Return a promise
		return _api.get( {
			action: 'query',
			titles: title,
			prop: 'revisions',
			rvprop: 'content',
			formatversion: 2
		} ).done( resolvedWithSuccess ).fail( resolvedWithFailure );
	}

	// Send a request to CheckWiki server
	IWRM.api.submitCheckWiki = ( title ) => {
		return $.get( 'https://checkwiki.toolforge.org/cgi-bin/checkwiki.cgi', {
			project: _c.wgDBname,
			view: 'detail',
			id: 68,
			title: title
		} );
	}

	// Get rate limits for the account
	IWRM.api.getRateLimits = () => {
		// Success
		function resolvedWithSuccess( data ) {
			var data_rl = OO.getProp( data, 'query', 'userinfo', 'ratelimits' );

			if ( data_rl ) {
				var limitsList = Object.keys( data_rl );
				if ( limitsList.length === 0 ) {
					IWRM.prefs.limit = _searchHighLimit;
					return IWRM.prefs.limit;
				}
			}

			return IWRM.prefs.limit;
		}

		// Failure
		function resolvedWithFailure( error, data ) {
			_ui.fn.notify( error, data, { replace: _locale.requests.getRateLimits } );

			return IWRM.prefs.limit;
		}

		// Return a promise
		return _api.get( {
			action: 'query',
			meta: 'userinfo',
			uiprop: 'ratelimits',
			formatversion: 2
		} ).then( resolvedWithSuccess ).fail( resolvedWithFailure );
	}

	// Get search request data
	IWRM.api.getSearchData = ( customList ) => {
		var didJustLoad = IWRM.data.title === '';
		var hasCustomList = typeof customList !== 'undefined';

		var searchOptions = {
			action: 'query',
			list: 'search',
			srprop: '',
			srwhat: 'text',
			srsearch: _searchRequest,
			srsort: 'last_edit_desc',
			sroffset: IWRM.offset,
			srlimit: IWRM.prefs.limit,
			formatversion: 2
		}

		// Success
		function resolvedWithSuccess( data ) {
			var data_sr = OO.getProp( data, 'query', 'search' );
			var offset = OO.getProp( data, 'continue', 'sroffset' );

			if ( data_sr ) {
				if ( offset ) {
					IWRM.offset = offset;
				}

				for ( var i = 0; i < data_sr.length; i++ ) {
					var title = data_sr[ i ].title;
					IWRM.list.push( {
						data: title
					} );
				}

				if ( didJustLoad ) {
					IWRM.data.title = data_sr[ 0 ].title;
				}
			}
		}

		// Failure
		function resolvedWithFailure( error, data ) {
			_ui.fn.notify( error, data, { replace: _locale.requests.getSearchData } );
		}

		// API call or a promise for custom list
		function doApiCall( searchOptions ) {
			if ( customList ) {
				IWRM.list = customList.map( el => {
					return { data: el };
				} );
				IWRM.data.title = customList[ 0 ];

				// Do a pseudo-call to something
				return mw.loader.using( 'mediawiki.util', () => {
					return IWRM.list;
				} );
			}

			return _api.get( searchOptions )
				.done( resolvedWithSuccess )
				.fail( resolvedWithFailure );
		}

		// Add different options for non site-wide search
		if ( !_isSiteWide ) {
			if ( _isCategory ) {
				// Request the pages in category
				searchOptions.srsearch = `${ searchOptions.srsearch } incategory:"${ _c.wgTitle }"`;
			} else if ( didJustLoad ) {
				// Request the current page
				searchOptions.srsearch = _c.wgPageName;
				searchOptions.srwhat = 'nearmatch';
			}
		}

		// Return a promise
		if ( didJustLoad ) {
			return IWRM.api.getRateLimits().then( ( limit ) => {
				searchOptions.srlimit = limit;
				return doApiCall( searchOptions );
			} );
		}

		return doApiCall( searchOptions );
	}

	// Request diff with changes
	IWRM.api.loadDiff = ( title ) => {
		if ( !title ) {
			_ui.fn.notify( 'script', null, {
				replace: _locale.requests.loadDiff,
				text: _locale.errors.noTitle
			} );
			return false;
		}

		// Failure
		function resolvedWithFailure( error, data ) {
			_ui.fn.notify( error, data, { replace: _locale.requests.loadDiff } );
		}

		// Return a promise
		return _api.post( {
			action: 'compare',
			fromtitle: title,
			totext: IWRM.data.modifiedText,
			formatversion: 2
		} ).fail( resolvedWithFailure );
	}

	// Submit changes
	IWRM.api.submitChanges = ( title, modified, summary ) => {
		if ( !title ) {
			_ui.fn.notify( 'script', null, {
				replace: _locale.requests.submitChanges,
				text: _locale.errors.noTitle,
				tag: 'iwrm-submit'
			} );
			return false;
		}

		if ( !modified ) {
			_ui.fn.notify( 'script', null, {
				replace: _locale.requests.submitChanges,
				text: _locale.errors.noText,
				tag: 'iwrm-submit'
			} );
			return false;
		}

		// Ensure that summary is present
		if ( !summary ) {
			summary = '';
		}

		// Success
		function resolvedWithSuccess( data ) {
			// Move counter up
			_counter += 1;

			// Send a request to CheckWiki server
			IWRM.api.submitCheckWiki( data.title || title );

			// Remove data for this article
			_ui.fn.removeArticle( title );

			// Show post-edit confirmation
			mw.config.set( {
				wgCurRevisionId: data.newrevid,
				wgRevisionId: data.newrevid,
			} );
			mw.hook( 'postEdit' ).fire();
		}

		// Failure
		function resolvedWithFailure( error, data ) {
			if ( error !== 'editconflict' ) {
				_ui.fn.notify( error, data, {
					replace: _locale.requests.submitChanges,
					tag: 'iwrm-submit'
				} );
				return;
			}

			// Try to submit once more for edit conflicts
			return doApiCall();
		}

		// Generic function for API call
		function doApiCall() {
			return _api.edit(
				title,
				( revision ) => {
					var text = _ui.fn.parseChanges( revision.content );
					if ( text === false ) {
						text = revision.content;
					}

					return {
						assertuser: _c.wgUserName,
						text: text,
						summary: summary,
						minor: true,
						watchlist: 'nochange',
						tags: IWRM.prefs.tag
					}
				}
			).done( resolvedWithSuccess ).fail( resolvedWithFailure );
		}

		// Return a promise
		return doApiCall();
	}

	/*
	 * Local functions
	 */

	// Get modified link text
	function getLink( obj, title ) {
		var text = obj.tmpl.text;
		if ( !text ) {
			text = '';
			
			// Use link in text if there is no shown text but titles differ
			if ( obj.tmpl.title && obj.tmpl.title.toLowerCase() !== title.toLowerCase() ) {
				text = obj.tmpl.title;
			}
		}

		var result = ( obj.lang ? _linkIsLang : _link );
		if ( title === IWRM.data.title ) {
			result = _linkTextOnly;
			if ( !text ) {
				text = title;
			}
		} else {
			// Add the missing pipe to text if there is any
			if ( text ) {
				text = '|' + text;
			}
		}
		result = result.replace( '$1', title ).replace( '$2', text );

		if ( obj.lang ) {
			// Have original title as text if there is none
			var langText = obj.lang.text;
			if ( !langText ) {
				langText = obj.tmpl.origTitle;
			}

			result = result.replace(
				'$3', obj.lang.lang
			).replace(
				'$4', langText.split( '_' ).join( ' ' )
			).replace(
				'$5', obj.lang.after
			);
		}

		return result;
	}

	// Get modified template text
	function getTemplate( obj ) {
		var result = ( obj.lang ? _tmplIsLang : _tmpl );
		result = result.replace(
			'$1', obj.tmpl.lang
		).replace(
			'$2', obj.tmpl.origTitle.split( '_' ).join( ' ' )
		).replace(
			'$3', obj.tmpl.text.split( '_' ).join( ' ' )
		);

		if ( obj.lang ) {
			result = result.replace(
				'$4', obj.tmpl.title.split( '_' ).join( ' ' )
			).replace(
				'$5', obj.lang.lang
			).replace(
				'$6', obj.lang.text.split( '_' ).join( ' ' )
			).replace(
				'$7', obj.lang.after
			);
		}

		return result;
	}

	// Get page index from a list, see getSearchData
	function getPageIndex( title ) {
		return IWRM.list.findIndex( ( page ) => {
			return page.data === title;
		} );
	}

	// Get page item from a list, see getSearchData
	function getPageTitle( index ) {
		return IWRM.list[ index ] ? IWRM.list[ index ].data : null;
	}

	// Check if OOUI element exists
	function ifUIElementsExist( El, does, doesnt ) {
		if ( Array.isArray( El ) ) {
			El.forEach( ( Element ) => {
				ifUIElementsExist( Element, does, doesnt )
			} );
			return;
		}

		if ( El ) {
			return ( typeof does !== 'undefined' ? does( El ) : true );
		}

		return ( typeof doesnt !== 'undefined' ? doesnt() : false );
	}

	// Normalise prefix data
	function normalisePrefix( lang, title ) {
		var normalPrefix = _normalisedPrefixes[ lang ];
		var normalTitle = title;
		if ( normalPrefix === 'en' || normalPrefix === _c.wgContentLanguage ) {
			var prefixMatch = _regularPrefix.exec( title );
			if ( prefixMatch !== null ) {
				normalPrefix = prefixMatch[ 1 ].toLowerCase();
				normalTitle = title.replace( _regularPrefix, '' );
			}
		}

		return {
			lang: normalPrefix,
			title: normalTitle
		}
	}

	// Get link data from a link
	function getLinkData( match ) {
		if ( match === null ) {
			return null;
		}

		var lang = match[ 1 ].toLowerCase();
		var title = match[ 2 ];
		var text = ( match[ 3 ] ? match[ 3 ] : '' );

		// Skip other projects
		if ( lang && _otherProjects.includes( lang ) ) {
			return null;
		}

		// Trim data
		lang = lang.trim();
		title = title.trim();
		text = text.trim();

		// Normalise prefixes
		if ( _normalisedPrefixKeys.includes( lang ) ) {
			var normalisedData = normalisePrefix( lang, title );
			lang = normalisedData.lang;
			title = normalisedData.title;
		}

		// Remove incorrect text
		if ( _regularBadText.exec( text ) ) {
			text = '';
		}

		return {
			lang: lang,
			title: title,
			text: text
		}
	}

	// Replace all links to templates
	function replaceLinks( text, callback ) {
		if ( !text ) {
			return false;
		}

		// Replace links with {{lang}} template around them
		_regularExpLangs.lastIndex = 0;
		var match = _regularExpLangs.exec( text );
		while ( match !== null ) {
			var link = ( match[ 1 ] ? match[ 1 ].split( '|' ) : [ '' ] );
			var langLang = match[ 2 ].toLowerCase();
			var langText = ( match[ 3 ] ? match[ 3 ] : '' );
			var after = ( match[ 4 ] ? match[ 4 ] : '' );

			_regularExp.lastIndex = 0;
			var textMatch = _regularExp.exec( langText );
			var linkData = getLinkData( textMatch );
			if ( linkData !== null ) {
				text = callback( {
					fullText: match[ 0 ],
					lang: {
						after: after,
						lang: langLang,
						text: linkData.text
					},
					tmpl: {
						lang: linkData.lang,
						origTitle: linkData.title,
						title: link[ 0 ],
						text: ( link[ 1 ] ? link[ 1 ] : '' )
					}
				}, text );
			}

			match = _regularExpLangs.exec( text );
		}

		// Replace regular links afterwards
		_regularExp.lastIndex = 0;
		match = _regularExp.exec( text );
		while ( match !== null ) {
			var linkData = getLinkData( match );
			if ( linkData !== null ) {
				text = callback( {
					fullText: match[ 0 ],
					tmpl: {
						lang: linkData.lang,
						origTitle: linkData.title,
						text: linkData.text
					}
				}, text );
			}
			match = _regularExp.exec( text );
		}

		return text;
	}

	/*
	 * Front-end
	 */

	// Clear all button
	_ui.ClearAll = () => {
		var Btn = new OO.ui.ButtonInputWidget( {
			disabled: true,
			label: _locale.clearAll
		} );

		// Set event listeners
		Btn.$button.on( 'click', () => {
			$( '.iwrm-clear-toggle' ).click();
			Btn.setLabel( Btn.getLabel() === _locale.clearAll ? _locale.clearAllRestore : _locale.clearAll );
		} );

		return Btn;
	}

	// Diff area
	_ui.Diff = function( Diff ) {
		// HTML parts for diff layout
		var diffLayout = '<table class="diff"><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content">';

		// Render an error if nothing was provided
		if ( !Diff ) {
			var $ErrorText = $( '<div>' ).addClass( 'error iwrm-error' ).text( _locale.errors.noLinks );

			// Enable action buttons
			_ui.fn.disableActions( false, true );

			// Check buttons on validity
			_ui.fn.checkBtns();

			return $ErrorText;
		}

		// Render the diff
		var $Diff = $( diffLayout );
		$Diff.append( Diff );

		return $Diff;
	}

	// Main dropdown
	_ui.Dropdown = () => {
		_FirstHeading.find( 'span' ).text( IWRM.data.title );
		_ui.PageLinks( IWRM.data.title );

		// Create the dropdown
		var Dropdown = new OO.ui.DropdownInputWidget( {
			disabled: true,
			options: IWRM.list
		} );

		// Change event callback
		function onDropdownChange( value ) {
			// Disable action buttons
			_ui.fn.disableActions( true );

			// Had no diff
			var hadNoDiff = IWRM.data.changeCount === 0;

			// Modify the title
			var oldTitle = IWRM.data.title;
			IWRM.data.title = value;

			if ( IWRM.data.title !== oldTitle ) {
				var title = IWRM.data.title;

				// Update counter
				var Counter = _FirstHeading.find( '.iwrm-counter' );
				if ( Counter.data( 'count' ) !== _counter ) {
					if ( Counter.hasClass( 'is-zero' ) && _counter !== 0 ) {
						Counter.removeClass( 'is-zero' );
						Counter.text( '−' + _counter );
					}

					Counter.data( 'count', _counter );
					if ( _counter !== 0 ) {
						Counter.text( '−' + _counter );
					}

					if ( _counter > _searchAutoUpdate ) {
						Counter.addClass( 'is-big' );
					}
				}

				// Update index
				IWRM.data.index = getPageIndex( title );

				// Update interface
				_FirstHeading.find( 'span' ).text( title );
				_ui.PageLinks( title );
				_ui.SiteSub( 0 );

				// Change summary back to default
				ifUIElementsExist(
					IWRM.ui.Summary,
					( El ) => {
						El.setValue( IWRM.prefs.summary );
					}
				);

				// Render new diff
				_ui.fn.renderDiff( title ).then( () => {
					// If there was a no links window, remove the previous item
					if ( hadNoDiff ) {
						_ui.fn.removeArticle( oldTitle );
					}

					// Load more data if needed
					if ( _searchAutoUpdate !== -1 && IWRM.list.length < _searchAutoUpdate + 1 ) {
						_ui.fn.getMoreData();
					}
				} );
			}
		}

		// Set event listeners
		Dropdown.on( 'change', onDropdownChange );

		return Dropdown;
	}

	// Editing interface
	_ui.EditingInterface = ( $Container, $Bar ) => {
		var $ChangedLines = $Container.find( '.diff-deletedline' );

		var promises = [];
		$ChangedLines.each( ( index, el ) => {
			promises.push(
				_ui.fn.modifyLine( index, el, $ChangedLines.length, $Container, $Bar )
			);
		} );

		Promise.all( promises ).then( () => {
			// Remove any stray lines
			$( '.iwrm-diff' ).find( '.diff-addedline' ).parent().remove();

			// Focus on first element
			var FirstInput = document.querySelector( '.iwrm-line input' );
			if ( FirstInput !== null ) {
				FirstInput.focus();
			}

			mw.hook( 'iwrm.content' ).fire( $Container );
		} );
	}

	// Footer
	_ui.Footer = () => {
		// Summary field
		IWRM.ui.Summary = new OO.ui.TextInputWidget( {
			value: IWRM.prefs.summary,
			title: _locale.summary,
			accessKey: 'b'
		} );

		// Submit on Enter
		IWRM.ui.Summary.$input.on( 'keydown', function( e ) {
			if ( e.keyCode === 13 ) {
				e.preventDefault();
				ifUIElementsExist(
					IWRM.ui.Submit,
					function( Element ) {
						Element.$button.click();
					}
				);
			}
		} );

		// Show byte limit
		var currentLimit = _c.wgCommentCodePointLimit - IWRM.prefs.summary.length;
		IWRM.ui.Summary.$input.codePointLimit( currentLimit );
		mw.widgets.visibleCodePointLimit( IWRM.ui.Summary, currentLimit );

		// UI buttons
		IWRM.ui.Submit = _ui.Submit();
		IWRM.ui.ClearAll = _ui.ClearAll();
		IWRM.ui.SkipPage = _ui.SkipPage();

		return new OO.ui.FieldsetLayout( {
			label: null,
			items: [
				new OO.ui.FieldLayout(
					IWRM.ui.Summary,
					{
						align: 'top',
						label: _locale.summary,
						errors: [ new OO.ui.HtmlSnippet( _locale.warrant ) ]
					}
				),
				new OO.ui.FieldLayout( new OO.ui.Widget( {
					content: [
						new OO.ui.HorizontalLayout( {
							items: [
								IWRM.ui.Submit,
								IWRM.ui.ClearAll,
								IWRM.ui.SkipPage
							]
						} )
					]
				} ) )
			]
		} );
	}

	// Input field
	_ui.InputField = ( obj, modified, entity, isTemplate ) => {
		var Input = new OO.ui.TextInputWidget( {
			placeholder: obj.fullText,
			value: modified,
			title: _locale.notice.moveUpDown
		} );
		Input.$input.data( 'old', obj.fullText );

		// Wikify the data (two times for link text transformations)
		Wikify( Input.$input[ 0 ] );
		Wikify( Input.$input[ 0 ] );

		// Render and add an input field
		var Field = new OO.ui.FieldLayout( Input, {
			align: 'top',
			label: null,
			content: [ _ui.InputLinks( Input, obj.tmpl.lang, obj.tmpl.origTitle, entity ) ]
		} );

		// Check if article exists in the project
		if ( isTemplate && obj.lang ) {
			_ui.fn.checkArticle( modified, Field );
		}

		var timer;
		Input.$input.on( 'blur', function() {
			var value = this.value.trim();

			clearTimeout( timer );
			_ui.fn.checkArticle( value, Field );
		} );
		Input.$input.on( 'input', function() {
			var value = this.value.trim();

			Field.setErrors( [] );
			clearTimeout( timer );
			timer = setTimeout( () => {
				_ui.fn.checkArticle( value, Field );
			}, 500 );
		} );

		// Keyboard shortcuts
		Input.$input.on( 'keydown', function( e ) {
			_ui.fn.onInputKeyDown( e );

			// Return to initial value on Esc
			if ( e.keyCode === 27 ) {
				e.preventDefault();
				this.value = this.defaultValue;
				_ui.fn.checkArticle( this.value, Field );
			}
		} );

		// Move cursor if value starts with our template or a link
		Input.$input.on( 'focus', function() {
			var value = this.value.trim();
			var start = -1;
			var end;
			if ( value.startsWith( _tmplStart ) ) {
				start = _tmplStart.length - 1;
			}

			// Select link text if it’s a link
			var endIndex = value.indexOf( _linkEnd );
			if ( endIndex !== -1 ) {
				var pipeIndex = value.indexOf( _linkPipe );
				start = pipeIndex === -1 ? endIndex : pipeIndex + 1;
				end = endIndex;
			}

			if ( start !== -1 ) {
				this.setSelectionRange( start, end || start );
			}
		} );

		return Field;
	}

	// Message with input links
	_ui.InputLinks = ( $Input, lang, title, entity ) => {
		var $LinksList = $( '<ul>' );
		
		var $MainLink = $( '<a class="extiw">' )
			.attr(
				'href',
				'https://$1.wikipedia.org'.replace( '$1', lang ) + mw.util.getUrl( title )
			)
			.attr( 'title', _locale.notice.interwiki )
			.attr( '_target', 'blank' )
			.text( '$1.wikipedia.org'.replace( '$1', lang ) );
		$LinksList.append( $( '<li> ').append( $MainLink ) );
		
		if ( entity ) {
			var $WikidataLink = $( '<a class="extiw">' )
				.attr(
					'href',
					'https://www.wikidata.org' + mw.util.getUrl( entity.id )
				)
				.attr( 'title', _locale.notice.wikidata )
				.attr( '_target', 'blank' )
				.text( 'wikidata.org' );

			$LinksList.append( $( '<li> ').append( $WikidataLink ) );

			// Add a link to local page if it is available
			var thiswiki = OO.getProp( entity, 'sitelinks', _c.wgDBname );
			if ( thiswiki ) {
				var $PageLink = $( '<a>' )
					.attr(
						'href',
						mw.util.getUrl( thiswiki.title )
					)
					.attr( 'title', _locale.notice.thiswiki )
					.attr( '_target', 'blank' )
					.text( _locale.notice.thiswikiArticle );

				$LinksList.append( $( '<li> ').append( $PageLink ) );
			}
		}

		// Add a link to clear/restore the input
		var oldValue = $Input.$input.attr( 'value' );
		var $ClearInputLink = $( '<a>' )
			.attr( 'role', 'button' )
			.attr( 'href', IWRM.prefs.hash + '#' )
			.addClass( 'iwrm-clear-toggle' )
			.text( _locale.notice.clearInput );
		$ClearInputLink.on( 'click', function( e ) {
			e.preventDefault();
			var value = $Input.getValue();
			if ( value !== '' ) {
				oldValue = value;
				$Input.setValue( '' );
				$( this ).text( _locale.notice.clearInputRestore );
			} else {
				$Input.setValue( oldValue );
				$( this ).text( _locale.notice.clearInput );
			}
		} );

		$ClearInputLink.on( 'keydown', function( e ) {
			if ( [ 'Enter', 'Space' ].includes( e.code ) ) {
				e.preventDefault();
				this.click();
			}
		} )

		$LinksList.append( $( '<li> ').append( $ClearInputLink ) );

		return $( '<div class="hlist hlist-items-nowrap">' ).append( $LinksList );
	}

	// Load more button
	_ui.LoadMore = () => {
		var Btn = new OO.ui.ButtonInputWidget( {
			flags: [ 'primary', 'progressive' ],
			icon: 'add',
			label: _locale.loadMore
		} );

		// Set event listeners
		Btn.$button.on( 'click', _ui.fn.getMoreData );

		return Btn;
	}

	// Nearby page button (previous / next)
	_ui.NearbyPage = ( type ) => {
		var Btn = new OO.ui.ButtonInputWidget( {
			disabled: true,
			flags: [ 'progressive' ],
			icon: type,
			label: _locale.nearbyPage[ type ],
			title: _locale.nearbyPage[ type ],
			accessKey: type === 'previous' ? 'a' : 'd'
		} );

		// Click event callback
		function onNearbyClick() {
			var index = IWRM.data.index;
			if ( type === 'previous' ) {
				index = ( index - 1 < 0 ? 0 : index - 1 );
			} else {
				index = ( index + 1 > IWRM.list.length - 1 ? IWRM.list.length - 1 : index + 1 );
			}

			// Set different value
			ifUIElementsExist(
				IWRM.ui.Dropdown,
				( El ) => {
					El.setValue( getPageTitle( index ) );
				}
			);
		}

		// Set event listeners
		Btn.$button.on( 'click', onNearbyClick );

		return Btn;
	}

	// Page links (tabs and edit section)
	_ui.PageLinks = ( title ) => {
		var $Element = $( '.mw-editsection' );
		var accessKeyHtml = ( _isSiteWide ? '' : ' accesskey="c"');
		var Html = '<span class="mw-editsection-bracket">[</span>' +
			'<a href="' + mw.util.getUrl( title ) + '" title="$1" target="_blank"' + accessKeyHtml + '>$2</a>' +
			'<span style="margin:0 0.25em;">|</span>' +
			'<a href="' + mw.util.getUrl( title, { action: 'edit' } ) + '" title="$3" target="_blank" accesskey="e">$4</a>' +
			'<span class="mw-editsection-bracket">]</span>';

		var htmlOpen = '<span class="mw-editsection mw-content-ltr iwrm-editsection" style="float: right;">';
		var htmlClose = '</span>';

		// Do the replacements
		Html = Html
			.replace( '$1', _locale.notice.viewArticle )
			.replace( '$2', _locale.viewArticle )
			.replace( '$3', _locale.notice.editSection )
			.replace( '$4', _locale.editSection );

		// Generic function to create tabs
		function createPageTab( data ) {
			var MainTab = mw.util.addPortletLink(
				_c.skin === 'vector-2022' ? 'p-associated-pages' : 'p-namespaces',
				data.href,
				data.title,
				data.id,
				data.hoverInfo,
				data.accessKey
			);
			
			return $( MainTab ).find( 'a' ).attr( 'target', '_blank' );
		}

		// Render two tabs if we are on the special page
		if ( _isSiteWide ) {
			var mainTabData = {
				id: 'ca-iwrm-main',
				title: _locale.mainTab,
				href: mw.util.getUrl( title ),
				hoverInfo: _locale.notice.viewArticle,
				accessKey: 'c'
			}
			var talkTabData = {
				id: 'ca-iwrm-talk',
				title: _locale.talkTab,
				href: mw.util.getUrl( _talkNs + title ),
				hoverInfo: _locale.notice.viewTalk,
				accessKey: 't'
			}

			var $MainTab = $( '#' + mainTabData.id );
			var $TalkTab = $( '#' + talkTabData.id );

			// Create article tab or set a new URL
			if ( $MainTab.length ) {
				$MainTab.find( 'a' ).attr( 'href', mainTabData.href );
			} else {
				$MainTab = createPageTab( mainTabData );
			}

			// Create talk tab or set a new URL
			if ( $TalkTab.length ) {
				$TalkTab.find( 'a' ).attr( 'href', talkTabData.href );
			} else {
				$TalkTab = createPageTab( talkTabData );
			}
		}

		// Render the result
		if ( $Element.length ) {
			$Element.addClass( 'iwrm-editsection' );
			$Element.html( Html );
			return;
		}

		if ( _SiteSub.length ) {
			Html = htmlOpen + Html + htmlClose;
			_SiteSub.before( Html );
		}
	}

	// Random page button
	_ui.RandomPage = () => {
		var Btn = new OO.ui.ButtonInputWidget( {
			disabled: true,
			icon: 'die',
			label: _locale.randomPage
		} );

		// Click event callback
		function onRandomClick() {
			var rand = Math.floor( Math.random() * ( IWRM.list.length - 1 - 0 + 1 ) ) + 0;

			ifUIElementsExist(
				IWRM.ui.Dropdown,
				( El ) => {
					El.setValue( getPageTitle( rand ) );
				}
			);
		}

		// Set event listeners
		Btn.$button.on( 'click', onRandomClick );

		return Btn;
	}

	// SiteSub
	_ui.SiteSub = ( count ) => {
		if ( typeof count === 'undefined' ) {
			count = IWRM.data.changeCount;
		}
		var text = _locale.title;
		if ( count === 0 ) {
			count = '';
		}
		if ( count === 1 ) {
			text = _locale.titleOne;
		}

		if ( count ) {
			count += ' ';
		}
		text = text.replace( '$1', count );
		
		// If ContentSub exists
		if ( _ContentSub.length ) {
			_ContentSub.empty();
		}

		// If SiteSub exists
		if ( _SiteSub.length ) {
			_SiteSub.text( text );
			return;
		}

		// If SiteSub doesn’t exist
		_ContentText.parent().prepend(
			$( '<div id="siteSub"></div>' ).text( text )
		);
		_SiteSub = $( '#siteSub' );
		return;
	}

	// Skip a page
	_ui.SkipPage = () => {
		var Btn = new OO.ui.ButtonInputWidget( {
			disabled: true,
			flags: [ 'destructive' ],
			framed: false,
			label: _locale.skipPage,
			title: _locale.skipPage,
			accessKey: 'i'
		} );

		// Set event listeners
		Btn.$button.on( 'click', () => {
			_ui.fn.removeArticle( IWRM.data.title );
		} );

		return Btn;
	}

	// Submit changes
	_ui.Submit = () => {
		var Btn = new OO.ui.ButtonInputWidget( {
			disabled: true,
			flags: [ 'primary', 'progressive' ],
			label: _locale.submit,
			title: _locale.submit,
			accessKey: 's'
		} );

		// Click event callback
		function onSubmitClick() {
			// Disable action buttons
			_ui.fn.disableActions( true );

			var modifiedText = _ui.fn.parseChanges( IWRM.data.text );

			// Show an error if user tries to submit a template with unchanged code
			if ( modifiedText === false ) {
				_ui.fn.notify( null, null, {
					text: _locale.errors.unchangedCode,
					tag: 'iwrm-unchanged'
				} );
				_ui.fn.disableActions( false );
				return;
			}

			// Change summary
			var summary = IWRM.prefs.summary;
			ifUIElementsExist(
				IWRM.ui.Summary,
				( El ) => {
					summary = El.getValue().trim();
				}
			);

			// Submit
			IWRM.api.submitChanges(
				IWRM.data.title,
				modifiedText,
				summary
			);
		}

		// Set event listeners
		Btn.$button.on( 'click', onSubmitClick );

		return Btn;
	}

	// A paragraph editing toggle
	_ui.Toggle = ( Textarea, $FauxLine ) => {
		var Toggle = new OO.ui.ToggleSwitchWidget();

		// Change event callback
		function onToggleChange( value ) {
			$FauxLine.toggleClass( 'is-editing-paragraph' );
			Textarea.setDisabled( !value );

			var textareaValue = Textarea.$input.data( 'old' );
			$FauxLine.find( '.iwrm-line input' ).each( ( index, el ) => {
				$( el ).attr( 'disabled', value );
				if ( value === true ) {
					var old = $( el ).data( 'old' );
					var elValue = $( el ).val().trim();

					if ( elValue !== '' ) {
						textareaValue = textareaValue.replace( old, elValue );
					}
					Textarea.focus();
				} else if ( index === 0 ) {
					$( el ).focus();
				}
			} );

			if ( value === true ) {
				Textarea.setValue( textareaValue );
			}
		}

		// Set event listeners
		Toggle.on( 'change', onToggleChange );

		return Toggle;
	}

	// Upper toolbar
	_ui.Toolbar = () => {
		// Should be loaded first and foremost
		IWRM.ui.Dropdown = _ui.Dropdown();

		// Other buttons
		IWRM.ui.LoadMore = _ui.LoadMore();
		IWRM.ui.PreviousPage = _ui.NearbyPage( 'previous' );
		IWRM.ui.NextPage = _ui.NearbyPage( 'next' );
		IWRM.ui.RandomPage = _ui.RandomPage();

		return new OO.ui.Widget( {
			classes: [ 'iwrm-toolbar' ],
			content: [
				new OO.ui.HorizontalLayout( {
					items: [
						IWRM.ui.LoadMore,
						IWRM.ui.PreviousPage,
						IWRM.ui.Dropdown,
						IWRM.ui.NextPage,
						IWRM.ui.RandomPage
					]
				} )
			]
		} );
	}

	// Check if article exists
	_ui.fn.checkArticle = ( value, Field ) => {
		var match = _regularTmpl.exec( value );
		var title = match && match[ 1 ];

		// Success
		function resolvedWithSuccess( data ) {
			var entity;
			var isMissing = OO.getProp( data, 'entities', '-1' );

			if ( !isMissing ) {
				var link = '<a href="' + mw.util.getUrl( title ) + '" title="$1" target="_blank">'.replace( '$1', _locale.notice.thiswiki );
				var error = _locale.errors.articleExists.replace( '$2', link ).replace( '$3', '</a>' ).replace( '$1', title );

				Field.setErrors( [
					new OO.ui.HtmlSnippet( error )
				] );
				return;
			}

			Field.setErrors( [] );
		}

		if ( title ) {
			IWRM.api.checkSitelinks(
				_c.wgContentLanguage,
				title
			).done( resolvedWithSuccess );
			return;
		}
	}

	// Check nearby pages on validity
	_ui.fn.checkBtns = () => {
		ifUIElementsExist(
			IWRM.ui.PreviousPage,
			( El ) => {
				El.setDisabled( IWRM.data.index === 0 );
			}
		);
		ifUIElementsExist(
			IWRM.ui.NextPage,
			( El ) => {
				El.setDisabled( IWRM.data.index === IWRM.list.length - 1 );
			}
		);

		// Random page button
		ifUIElementsExist(
			IWRM.ui.RandomPage,
			( El ) => {
				El.setDisabled( IWRM.list.length === 1 );
			}
		);
	}

	// Enable and disable action buttons
	_ui.fn.disableActions = ( value, submitValue ) => {
		// Disable according to value
		const toggleAction = ( El ) => {
			El.setDisabled( value );
		}

		// Nearby pages buttons
		if ( value === true ) {
			ifUIElementsExist(
				[
					IWRM.ui.PreviousPage,
					IWRM.ui.NextPage,
					IWRM.ui.RandomPage
				],
				toggleAction
			);
		} else {
			_ui.fn.checkBtns();
		}

		// Dropdown and footer buttons
		ifUIElementsExist(
			[
				IWRM.ui.Dropdown,
				IWRM.ui.ClearAll,
				IWRM.ui.SkipPage
			],
			toggleAction
		);

		ifUIElementsExist(
			IWRM.ui.Submit,
			( El ) => {
				var val = typeof submitValue === 'undefined' ? value : submitValue;
				El.setDisabled( val );
			}
		);
	}

	// Get more data
	_ui.fn.getMoreData = () => {
		// Disable Load more button
		ifUIElementsExist(
			IWRM.ui.LoadMore,
			( El ) => {
				El.setDisabled( true );
			}
		);

		IWRM.api.getSearchData().then( () => {
			// Update data in dropdown
			ifUIElementsExist(
				IWRM.ui.Dropdown,
				( El ) => {
					El.setOptions( IWRM.list );
				}
			);

			// Enable Load more button and update text
			ifUIElementsExist(
				IWRM.ui.LoadMore,
				( El ) => {
					El.setDisabled( false );
				}
			);

			// Check buttons on validity
			_ui.fn.checkBtns();
		} );
	}

	// Modify a changed line
	_ui.fn.modifyLine = ( index, El, length, $Container, $Bar ) => {
		var $El = $( El );
		var $Parent = $El.parent();

		// Find a future container for inputs
		var $FauxLine = $Parent.find( '.diff-addedline' );
		if ( $FauxLine.length === 0 ) {
			$FauxLine = $Parent.find( '.diff-empty' );

			if ( $FauxLine.length > 0 ) {
				$FauxLine.removeAttr( 'colspan' );
				$FauxLine.before( '<td class="diff-marker"></td>' );

				$Parent.next().remove();
			}
		}

		// Modify input container
		$FauxLine.removeClass( 'diff-addedline' ).removeClass( 'diff-empty' ).addClass( 'iwrm-fauxline' );
		var Html = '<div class="iwrm-paragraph"></div><div class="iwrm-line"></div><div class="iwrm-toggle"></div>';
		$FauxLine.html( Html );

		var $InputHolder = $FauxLine.find( '.iwrm-line' );

		// Render input fields for each change
		var text = $El.text();
		var InputFields = [];
		replaceLinks( text, ( obj, text ) => {
			var index = text.indexOf( obj.fullText );
			InputFields[ index ] = _ui.fn.renderInput( obj );
			IWRM.data.changeCount++;

			// Return the dummy modified text to go to next input
			var modified = getTemplate( obj );
			return text.replace( obj.fullText, modified );
		} );

		// Append them all at once synchronously
		var promises = Promise.all( InputFields ).then( ( Element ) => {
			$InputHolder.append( Element );
		} ).then( () => {
			// Remove progress bar and show the diff
			if ( index === length - 1 ) {
				$Bar.$element.remove();
				$Container.addClass( IWRM.prefs.loaded );

				// Check buttons on validity
				_ui.fn.checkBtns();

				// Enable action buttons
				_ui.fn.disableActions( false );
				
				// Update link count
				_ui.SiteSub();
			}
		} );

		// Render a hidden textarea
		var Textarea = new OO.ui.MultilineTextInputWidget( {
			autosize: true,
			disabled: true,
			title: _locale.notice.moveUpDown
		} );
		Textarea.$input.data( 'old', text );
		$FauxLine.find( '.iwrm-paragraph' ).append( Textarea.$element );

		// Keyboard shortcuts
		Textarea.$input.on( 'keydown', _ui.fn.onInputKeyDown );

		// Render a toggle for changing states
		var toggle = new OO.ui.FieldLayout(
			_ui.Toggle( Textarea, $FauxLine ),
			{
				label: _locale.editParagraph
			}
		);
		$FauxLine.find( '.iwrm-toggle' ).append( toggle.$element );

		return promises;
	}

	// Render an input
	_ui.fn.renderInput = ( obj ) => {
		var modified = getTemplate( obj );

		// Success
		function resolvedWithSuccess( data, isTemplate ) {
			var entity;
			var isMissing = OO.getProp( data, 'entities', '-1' );

			// For returning a link to the same language
			var title = obj.tmpl.origTitle;

			if ( !isMissing ) {
				entity = OO.getProp( data, 'entities' );
				if ( entity ) {
					var keys = Object.keys( entity );
					entity = entity[ keys[ 0 ] ];
				}

				// Change the output if a local page is available
				var data_sl = OO.getProp( entity, 'sitelinks', _c.wgDBname );
				if ( data_sl ) {
					title = data_sl.title;
					isTemplate = false;
				}
			}

			if ( isTemplate === false ) {
				modified = getLink( obj, title );
			}
			
			var InputField = _ui.InputField( obj, modified, entity, isTemplate );
			return $.Deferred().resolve( InputField.$element );
		}

		// Failure
		function resolvedWithFailure() {
			IWRM.data.changeCount--;

			// Add an error message
			return $.Deferred().resolve( '<div class="iwrm-error">$1</div>'.replace(
				'$1',
				_locale.errors.badInterwiki.replace( '$1', obj.tmpl.lang )
			) );
		}

		// Render a link on links from the same wiki
		if ( obj.tmpl.lang === _c.wgContentLanguage ) {
			const fakeData = { entities: { '-1': { data: 'fake' } } };

			return resolvedWithSuccess( fakeData, false );
		}

		// Return a promise
		return IWRM.api.checkSitelinks(
			obj.tmpl.lang,
			obj.tmpl.origTitle
		).then( resolvedWithSuccess, resolvedWithFailure );
	}

	// React to keydown events in the same fashion
	_ui.fn.onInputKeyDown = function( e ) {
		// Submit on (Ctrl|Alt)+Enter or (Ctrl|Alt)+S
		if ( ( e.ctrlKey || e.altKey ) && !e.shiftKey && ( e.code === 'Enter' || e.code === 'KeyS' ) ) {
			e.preventDefault();
			ifUIElementsExist(
				IWRM.ui.Submit,
				( El ) => {
					El.$button.click();
					_notification = _ui.fn.notify( null, null, {
						text: _locale.errors.saving.replace( '$1', IWRM.data.title ),
						tag: 'iwrm-keydown'
					} );
				}
			);
		}

		// (Ctrl|Alt)+(Shift+|)(J|K) (Alt+J/Alt+K|Ctrl+J/Alt+K) to move between fields
		var goToPrev = e.code === 'KeyJ';
		var goToNext = e.code === 'KeyK';
		if ( ( e.ctrlKey || e.altKey ) && ( goToPrev || goToNext ) ) {
			e.preventDefault();
			var inputs = document.querySelectorAll( '.iwrm-diff input:not([disabled]), .iwrm-diff textarea:not([disabled])' );
			if ( inputs.length <= 1 ) {
				_notification = _ui.fn.notify( null, null, {
					text: _locale.errors.altOnlyLink,
					tag: 'iwrm-keydown'
				} );
				return;
			}
			var thisInput = e.target;
			var index = 0;
			var chosenIndex = null;
			while ( index < inputs.length ) {
				if ( inputs[ index ] === thisInput ) {
					chosenIndex = index;
					break;
				}
				index++;
			}

			function notifyIfNoNextInput() {
				if ( goToPrev ) return;

				_notification = _ui.fn.notify( null, null, {
					text: _locale.errors.altNoNext,
					tag: 'iwrm-keydown'
				} );
			}

			if ( chosenIndex === null ) {
				notifyIfNoNextInput();
				return;
			}

			var nextIndex = goToNext ? chosenIndex + 1 : chosenIndex - 1;
			ifUIElementsExist(
				inputs[ nextIndex ],
				( El ) => {
					El.focus();
				},
				notifyIfNoNextInput
			);
		}
	}

	// Parse changes in the diff view
	_ui.fn.parseChanges = ( content ) => {
		var result = content;
		var hasErrors = false;
		var $Changes = $(
			'.iwrm-paragraph textarea:not(:disabled), '
			+ '.iwrm-line input:not(:disabled)'
		);
		$Changes.each( ( index, el ) => {
			var old = $( el ).data( 'old' );
			var value = $( el ).val();

			// Do not trim textarea, since there can be meaningful spaces
			if ( $( el ).prop( 'tagName' ) === 'INPUT' ) {
				value = value.trim();
			}

			if ( value !== '' || $( el ).prop( 'tagName' ) === 'TEXTAREA' ) {
				// Check if there is unchanged code anywhere
				if ( value.includes( _tmplStart ) ) {
					hasErrors = true;
					return false;
				}
				var unmodified = result;

				// Remove entire paragraph for textarea
				if ( value === '' ) {
					result = result.replace( old + '\n', value );
					unmodified = result;
				}

				// Remove other text if no modifications were made
				if ( unmodified === result ) {
					result = result.replace( old, value );
				}
			}
		} );

		if ( result === content ) {
			return false;
		}

		return hasErrors === true ? false : result;
	}

	// Remove an article from the lists
	_ui.fn.removeArticle = ( title ) => {
		// Calculate the index and update the lists
		var index = getPageIndex( title );
		if ( index === -1 ) {
			return;
		}

		IWRM.list.splice( index, 1 );
		
		// Set new data to dropdown and update the picked element
		var newIndex = ( index + 1 > IWRM.list.length - 1 ? IWRM.list.length - 1 : index );
		ifUIElementsExist(
			IWRM.ui.Dropdown,
			( El ) => {
				El.setOptionsData( IWRM.list );
				El.setValue( getPageTitle( newIndex ) );
				IWRM.data.index = newIndex;
			}
		);
	}

	// Render a diff
	_ui.fn.renderDiff = ( title ) => {
		var $Container = $( '.iwrm-diff' );
		var $DiffBar = new OO.ui.ProgressBarWidget( {
			progress: false
		} );

		// Render a progress bar
		$Container.removeClass( IWRM.prefs.loaded );
		$Container.html( $DiffBar.$element );

		return IWRM.api.getFullText( title ).then( () => {
			return IWRM.api.loadDiff( title ).then( ( data ) => {
				// Insert the diff or the lack of it
				var DiffResult = OO.getProp( data, 'compare', 'body' );
				var Diff = _ui.Diff( DiffResult );
				$Container.append( Diff );

				// Start rendering replacement interface
				IWRM.data.changeCount = 0;
				if ( DiffResult ) {
					IWRM.ui.Diff = Diff;
					_ui.EditingInterface( $Container, $DiffBar );
				} else {
					IWRM.ui.Diff = null;
					throw new Error();
				}

				IWRM.data.modifiedText = '';
			} );
		} ).catch( () => {
			// Remove progress bar and show the message
			$DiffBar.$element.remove();
			$Container.addClass( IWRM.prefs.loaded );
		} );
	}

	// Show a notification
	_ui.fn.notify = ( error, data, options ) => {
		var text = options ? options.text : '';
		var replace = options ? options.replace : '';

		// Add a default text
		if ( !text ) {
			text = _locale.errors.unidentified;
		}

		if ( error === 'http' ) {
			text = _locale.errors.noNetwork;
		}

		if ( error === 'missingtitle' ) {
			text = _locale.errors.noArticle;
		}

		// Show up some additional text to keep text different
		if ( replace ) {
			text = text.replace( '$1', replace );
		}

		// Get reasoning for showing the popup
		if ( error ) {
			text = _locale.errors.text + text;
		}

		return mw.notification.notify( text, {
			autoHide: ( error ? false : true ),
			title: ( error ? _locale.errors.title : '' ),
			tag: ( options && options.tag ? options.tag : null )
		} );
	}

	/*
	 * Initialising a portlet
	 */
	IWRM.InitPortlet = () => {
		var namespaces = [
			0,
			14, // Category
		]
		if ( !namespaces.includes( _c.wgNamespaceNumber ) && _c.wgTitle !== 'Песочница' || mw.config.get( 'wgIsMainPage' ) ) {
			return;
		}

		if ( _c.wgAction !== 'view' ) {
			return;
		}

		window.addEventListener( 'hashchange', () => {
			IWRM.Init();
		} );

		var $iwLinks = $( '.mw-parser-output a.extiw' );
		if ( _c.wgNamespaceNumber === 0 && $iwLinks.length === 0 ) {
			return;
		}

		mw.loader.using( 'mediawiki.util', () => {
			var portlet = mw.util.addPortletLink(
				'p-tb',
				IWRM.prefs.hash + _c.wgPageName,
				_locale.title.replace( '$1', '' ),
				't-iwrm',
				IWRM.prefs.name
			);
		} );
	}

	/*
	 * Initialising
	 */
	IWRM.Init = ( customList ) => {
		// Check hash and user groups
		var from = window.location.hash;
		if ( !from.startsWith( IWRM.prefs.hash ) ) {
			if ( _isSiteWide ) {
				console.warn( IWRM.prefs.name + ': please re-open the page with ' + IWRM.prefs.hash + ' if you intended to use the script' );
			}
			return;
		}

		if ( !_isAllowed ) {
			_ui.fn.notify( 'script', null, { text: _locale.errors.notAllowed } );
			return;
		}

		// Change page title
		if ( _isSiteWide ) {
			document.title = document.title.replace( _c.wgTitle, _locale.title.replace( '$1', '' ) );
		}
		_FirstHeading.text( '' );
		_FirstHeading.append( $( '<small>', { class: 'iwrm-counter' + ' is-zero' } ).text( _counter ) );
		_FirstHeading.append( $( '<span>' ).text( _locale.loadingTitle ) );
		_ui.SiteSub( 0 );

		// Start rendering the interface
		_ContentText.addClass( 'mw-parser-output' ).addClass( 'iwrm' );
		_ContentText.empty();
		_Indicators.empty();
		$( '.iwrm' ).removeClass( IWRM.prefs.loaded );

		mw.loader.using( _deps ).done( () => {
			_api = new mw.Api();
			_wikidataApi = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );

			// Render a progress bar
			var $ContentBar = new OO.ui.ProgressBarWidget( {
				progress: false
			} );
			_ContentText.append( $ContentBar.$element );

			_notification = _ui.fn.notify( null, null, {
				text: _locale.loading,
				replace: IWRM.prefs.name
			} );

			IWRM.api.getSearchData( customList ).then( () => {
				// Render toolbar once
				_ContentText.append( _ui.Toolbar().$element );

				// Preload diff area
				_ContentText.append( $( '<div>', { class: 'iwrm-diff' } ) );

				// Render footer
				_ContentText.append( _ui.Footer().$element );

				// Remove progress bar and show the page
				_ui.fn.renderDiff( IWRM.data.title ).then( () => {
					$ContentBar.$element.remove();
					_ContentText.addClass( IWRM.prefs.loaded );
				} );

				// Confirm before leaving with a hash
				mw.confirmCloseWindow( {
					test: () => {
						return window.location.hash.startsWith( IWRM.prefs.hash );
					}
				} );

				// Remove notifications on saving
				mw.hook( 'postEdit' ).add( () => {
					if ( _notification ) {
						_notification = _notification.close();
					}
				} );
			} );
		} ).fail( ( error, data ) => {
			_ui.fn.notify( error, data, { replace: _locale.requests.init } );
		} );
	}

	/* </nowiki>
	 * Starting point
	 */
	IWRM.Init();
	if ( !_isSiteWide ) {
		IWRM.InitPortlet();
	}
}() );