MediaWiki:Gadget-iwrm.js: различия между версиями

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Содержимое удалено Содержимое добавлено
fix
уточнение проверки на разлогин
 
(не показано 13 промежуточных версий этого же участника)
Строка 57: Строка 57:
'wgTitle',
'wgTitle',
'wgUserGroups',
'wgUserGroups',
'wgUserName'
'wgUserName',
] );
] );


// Special:BlankPage or individual page
// Special:BlankPage, category or individual page
var _isSiteWide = true;
var _isSiteWide = true;
var _isCategory = _c.wgNamespaceNumber === 14;
if ( _c.wgCanonicalSpecialPageName !== 'Blankpage' ) {
if ( _c.wgCanonicalSpecialPageName !== 'Blankpage' ) {
_isSiteWide = false;
_isSiteWide = false;
Строка 141: Строка 142:


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


// Expression for interwiki links in language: [[$1]] ({{lang-$2|$3}}$4
// Expression for interwiki links in language: [[$1]] ({{lang-$2|$3}}$4
Строка 147: Строка 148:


// Expression for prefixes: [[:$1:]]
// Expression for prefixes: [[:$1:]]
var _regularPrefix = /:?([a-z-]+):/gi;
var _regularPrefix = / *:? *([a-z-]+) *: */gi;
// Expression for templates: {{iw}}, {{нп5}}, {{не переведено 5}}
// Expression for templates: {{iw}}, {{нп5}}, {{не переведено 5}}
Строка 204: Строка 205:
notAllowed: 'Чтобы не распатрулировать статьи, скрипт можно использовать только автопатрулируемым, патрулирующим и администраторам.',
notAllowed: 'Чтобы не распатрулировать статьи, скрипт можно использовать только автопатрулируемым, патрулирующим и администраторам.',


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


// Check sitelinks
// Check sitelinks
IWRM.api.checkSitelinks = function( lang, title ) {
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( {
return _wikidataApi.get( {
action: 'wbgetentities',
action: 'wbgetentities',
sites: lang + 'wiki',
sites: getDBname( lang ),
sitefilter: _c.wgDBname,
sitefilter: _c.wgDBname,
titles: title,
titles: title,
Строка 288: Строка 297:


// Get full article text
// Get full article text
IWRM.api.getFullText = function( title ) {
IWRM.api.getFullText = ( title ) => {
if ( !title ) {
if ( !title ) {
_ui.fn.notify( 'script', null, {
_ui.fn.notify( 'script', null, {
Строка 299: Строка 308:
// Success
// Success
function resolvedWithSuccess( data ) {
function resolvedWithSuccess( data ) {
var revision = data && data.query && data.query.pages && data.query.pages[ 0 ] && data.query.pages[ 0 ].revisions && data.query.pages[ 0 ].revisions[ 0 ] && data.query.pages[ 0 ].revisions[ 0 ];
var revision = OO.getProp( data, 'query', 'pages', 0, 'revisions', 0 );


if ( revision ) {
if ( revision ) {
Строка 305: Строка 314:


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


Строка 313: Строка 322:
}
}


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


Строка 335: Строка 345:


// Send a request to CheckWiki server
// Send a request to CheckWiki server
IWRM.api.submitCheckWiki = function( title ) {
IWRM.api.submitCheckWiki = ( title ) => {
return $.get( 'https://checkwiki.toolforge.org/cgi-bin/checkwiki.cgi', {
return $.get( 'https://checkwiki.toolforge.org/cgi-bin/checkwiki.cgi', {
project: _c.wgDBname,
project: _c.wgDBname,
Строка 345: Строка 355:


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


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


return IWRM.data.limit;
return IWRM.prefs.limit;
}
}


Строка 364: Строка 375:
_ui.fn.notify( error, data, { replace: _locale.requests.getRateLimits } );
_ui.fn.notify( error, data, { replace: _locale.requests.getRateLimits } );


return IWRM.data.limit;
return IWRM.prefs.limit;
}
}


Строка 377: Строка 388:


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


var searchOptions = {
var searchOptions = {
Строка 390: Строка 399:
srsearch: _searchRequest,
srsearch: _searchRequest,
srsort: 'last_edit_desc',
srsort: 'last_edit_desc',
sroffset: offset,
sroffset: IWRM.offset,
srlimit: IWRM.prefs.limit,
srlimit: IWRM.prefs.limit,
formatversion: 2
formatversion: 2
Строка 397: Строка 406:
// Success
// Success
function resolvedWithSuccess( data ) {
function resolvedWithSuccess( data ) {
var data_sr = data && data.query && data.query.search;
var data_sr = OO.getProp( data, 'query', 'search' );
var offset = OO.getProp( data, 'continue', 'sroffset' );


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

for ( var i = 0; i < data_sr.length; i++ ) {
for ( var i = 0; i < data_sr.length; i++ ) {
var title = data_sr[ i ].title;
var title = data_sr[ i ].title;
Строка 418: Строка 432:
}
}


// Request the current page for non site-wide search
// API call or a promise for custom list
function doApiCall( searchOptions ) {
if ( !_isSiteWide && didJustLoad ) {
if ( customList ) {
searchOptions.srsearch = _c.wgPageName;
IWRM.list = customList.map( el => {
searchOptions.srwhat = 'nearmatch';
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
// Return a promise
if ( didJustLoad ) {
if ( didJustLoad ) {
return IWRM.api.getRateLimits().then( function( limit ) {
return IWRM.api.getRateLimits().then( ( limit ) => {
searchOptions.srlimit = limit;
searchOptions.srlimit = limit;
return _api.get( searchOptions ).done( resolvedWithSuccess ).fail( resolvedWithFailure );
return doApiCall( searchOptions );
} );
} );
}
}


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


// Request diff with changes
// Request diff with changes
IWRM.api.loadDiff = function( title ) {
IWRM.api.loadDiff = ( title ) => {
if ( !title ) {
if ( !title ) {
_ui.fn.notify( 'script', null, {
_ui.fn.notify( 'script', null, {
Строка 460: Строка 499:


// Submit changes
// Submit changes
IWRM.api.submitChanges = function( title, modified, summary ) {
IWRM.api.submitChanges = ( title, modified, summary ) => {
if ( !title ) {
if ( !title ) {
_ui.fn.notify( 'script', null, {
_ui.fn.notify( 'script', null, {
Строка 482: Строка 521:
if ( !summary ) {
if ( !summary ) {
summary = '';
summary = '';
}

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

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


Строка 511: Строка 529:


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


// Remove data for this article
// Remove data for this article
Строка 517: Строка 535:


// Show post-edit confirmation
// Show post-edit confirmation
mw.config.set( {
wgCurRevisionId: data.newrevid,
wgRevisionId: data.newrevid,
} );
mw.hook( 'postEdit' ).fire();
mw.hook( 'postEdit' ).fire();
}
}
Строка 532: Строка 554:
// Try to submit once more for edit conflicts
// Try to submit once more for edit conflicts
return doApiCall();
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 );
}
}


Строка 615: Строка 659:
// Get page index from a list, see getSearchData
// Get page index from a list, see getSearchData
function getPageIndex( title ) {
function getPageIndex( title ) {
return IWRM.list.findIndex( function( page ) {
return IWRM.list.findIndex( ( page ) => {
return page.data === title;
return page.data === title;
} );
} );
Строка 626: Строка 670:


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


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


Строка 642: Строка 686:


// Normalise prefix data
// Normalise prefix data
function normalise( lang, title ) {
function normalisePrefix( lang, title ) {
var normalPrefix = _normalisedPrefixes[ lang ];
var normalPrefix = _normalisedPrefixes[ lang ];
var normalTitle = title;
var normalTitle = title;
if ( normalPrefix === 'en' ) {
if ( normalPrefix === 'en' || normalPrefix === _c.wgContentLanguage ) {
var prefixMatch = _regularPrefix.exec( title );
var prefixMatch = _regularPrefix.exec( title );
if ( prefixMatch !== null ) {
if ( prefixMatch !== null ) {
Строка 681: Строка 725:
// Normalise prefixes
// Normalise prefixes
if ( _normalisedPrefixKeys.includes( lang ) ) {
if ( _normalisedPrefixKeys.includes( lang ) ) {
var normalisedData = normalise( lang, title );
var normalisedData = normalisePrefix( lang, title );
lang = normalisedData.lang;
lang = normalisedData.lang;
title = normalisedData.title;
title = normalisedData.title;
Строка 762: Строка 806:


// Clear all button
// Clear all button
_ui.ClearAll = function() {
_ui.ClearAll = () => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
disabled: true,
disabled: true,
Строка 769: Строка 813:


// Set event listeners
// Set event listeners
Btn.$button.on( 'click', function() {
Btn.$button.on( 'click', () => {
$( '.iwrm-clear-toggle' ).click();
$( '.iwrm-clear-toggle' ).click();
Btn.setLabel( Btn.getLabel() === _locale.clearAll ? _locale.clearAllRestore : _locale.clearAll );
Btn.setLabel( Btn.getLabel() === _locale.clearAll ? _locale.clearAllRestore : _locale.clearAll );
Строка 778: Строка 822:


// Diff area
// Diff area
_ui.Diff = function( diff ) {
_ui.Diff = function( Diff ) {
// HTML parts for diff layout
// 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">';
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
// Render an error if nothing was provided
if ( !diff ) {
if ( !Diff ) {
var $ErrorText = $( '<div>' ).addClass( 'error iwrm-error' ).text( _locale.errors.noLinks );
var $ErrorText = $( '<div>' ).addClass( 'error iwrm-error' ).text( _locale.errors.noLinks );


Строка 797: Строка 841:
// Render the diff
// Render the diff
var $Diff = $( diffLayout );
var $Diff = $( diffLayout );
$Diff.append( diff );
$Diff.append( Diff );


return $Diff;
return $Diff;
Строка 803: Строка 847:


// Main dropdown
// Main dropdown
_ui.Dropdown = function() {
_ui.Dropdown = () => {
_FirstHeading.find( 'span' ).text( IWRM.data.title );
_FirstHeading.find( 'span' ).text( IWRM.data.title );
_ui.PageLinks( IWRM.data.title );
_ui.PageLinks( IWRM.data.title );
Строка 857: Строка 901:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Summary,
IWRM.ui.Summary,
function( el ) {
( El ) => {
el.setValue( IWRM.prefs.summary );
El.setValue( IWRM.prefs.summary );
}
}
);
);


// Render new diff
// Render new diff
_ui.fn.renderDiff( title ).then( function() {
_ui.fn.renderDiff( title ).then( () => {
// If there was a no links window, remove the previous item
// If there was a no links window, remove the previous item
if ( hadNoDiff ) {
if ( hadNoDiff ) {
Строка 884: Строка 928:


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


var promises = [];
var promises = [];
$ChangedLines.each( function( index, el ) {
$ChangedLines.each( ( index, el ) => {
promises.push(
promises.push(
_ui.fn.modifyLine( index, el, $ChangedLines.length, $Container, $Bar )
_ui.fn.modifyLine( index, el, $ChangedLines.length, $Container, $Bar )
Строка 894: Строка 938:
} );
} );


Promise.all( promises ).then( function() {
Promise.all( promises ).then( () => {
// Remove any stray lines
// Remove any stray lines
$( '.iwrm-diff' ).find( '.diff-addedline' ).parent().remove();
$( '.iwrm-diff' ).find( '.diff-addedline' ).parent().remove();
Строка 900: Строка 944:
// Focus on first element
// Focus on first element
var FirstInput = document.querySelector( '.iwrm-line input' );
var FirstInput = document.querySelector( '.iwrm-line input' );
if ( FirstInput !== null ) {
FirstInput.focus();
FirstInput.focus();
}


mw.hook( 'iwrm.content' ).fire( $Container );
mw.hook( 'iwrm.content' ).fire( $Container );
Строка 907: Строка 953:


// Footer
// Footer
_ui.Footer = function() {
_ui.Footer = () => {
// Summary field
// Summary field
IWRM.ui.Summary = new OO.ui.TextInputWidget( {
IWRM.ui.Summary = new OO.ui.TextInputWidget( {
Строка 921: Строка 967:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Submit,
IWRM.ui.Submit,
function( element ) {
function( Element ) {
element.$button.click();
Element.$button.click();
}
}
);
);
Строка 965: Строка 1011:


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


// Wikify the data (two times for link text transformations)
// Success
Wikify( Input.$input[ 0 ] );
function resolvedWithSuccess( data ) {
Wikify( Input.$input[ 0 ] );
var entity;
var isMissing = data && data.entities && data.entities[ '-1' ];
if ( !isMissing ) {
entity = data && data.entities;
if ( entity ) {
var keys = Object.keys( entity );
entity = entity[ keys[ 0 ] ];
}


// Render and add an input field
// Change the output if a local page is available
var Field = new OO.ui.FieldLayout( Input, {
var data_sl = entity.sitelinks && entity.sitelinks[ _c.wgDBname ];
align: 'top',
if ( data_sl ) {
label: null,
modified = getLink( obj, data_sl.title );
content: [ _ui.InputLinks( Input, obj.tmpl.lang, obj.tmpl.origTitle, entity ) ]
isTemplate = false;
}
} );
}


// Create the input
// Check if article exists in the project
if ( isTemplate && obj.lang ) {
var Input = new OO.ui.TextInputWidget( {
_ui.fn.checkArticle( modified, Field );
placeholder: obj.fullText,
}
value: modified,
title: _locale.notice.moveUpDown
} );
Input.$input.data( 'old', obj.fullText );


var timer;
// Wikify the data (two times for link text transformations)
Wikify( Input.$input[ 0 ] );
Input.$input.on( 'blur', function() {
var value = this.value.trim();
Wikify( Input.$input[ 0 ] );


clearTimeout( timer );
// Render and add an input field
_ui.fn.checkArticle( value, Field );
var Field = new OO.ui.FieldLayout( Input, {
} );
align: 'top',
Input.$input.on( 'input', function() {
label: null,
var value = this.value.trim();
content: [ _ui.InputLinks( Input, obj.tmpl.lang, obj.tmpl.origTitle, entity ) ]
} );


Field.setErrors( [] );
// Check if article exists in the project
clearTimeout( timer );
if ( isTemplate && obj.lang ) {
timer = setTimeout( () => {
_ui.fn.checkArticle( modified, Field );
}

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

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


// Keyboard shortcuts
Field.setErrors( [] );
Input.$input.on( 'keydown', function( e ) {
clearTimeout( timer );
_ui.fn.onInputKeyDown( e );
timer = setTimeout( function() {
_ui.fn.checkArticle( value, Field );
}, 500 );
} );


// Return to initial value on Esc
// Keyboard shortcuts
Input.$input.on( 'keydown', function( e ) {
if ( e.keyCode === 27 ) {
_ui.fn.onInputKeyDown( e );
e.preventDefault();
this.value = this.defaultValue;
_ui.fn.checkArticle( this.value, Field );
}
} );


// Return to initial value on Esc
// Move cursor if value starts with our template or a link
Input.$input.on( 'focus', function() {
if ( e.keyCode === 27 ) {
var value = this.value.trim();
e.preventDefault();
var start = -1;
this.value = this.defaultValue;
var end;
_ui.fn.checkArticle( this.value, Field );
if ( value.startsWith( _tmplStart ) ) {
}
start = _tmplStart.length - 1;
} );
}


// Move cursor if value starts with our template or a link
// Select link text if it’s a link
var endIndex = value.indexOf( _linkEnd );
Input.$input.on( 'focus', function() {
if ( endIndex !== -1 ) {
var value = this.value.trim();
var start = -1;
var pipeIndex = value.indexOf( _linkPipe );
start = pipeIndex === -1 ? endIndex : pipeIndex + 1;
var end;
end = endIndex;
if ( value.startsWith( _tmplStart ) ) {
}
start = _tmplStart.length - 1;
}


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


return Field;
if ( start !== -1 ) {
this.setSelectionRange( start, end || start );
}
} );
return $.Deferred().resolve( Field.$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 )
) );
}

// Normalise language prefix for Wikidata
var wdLang = obj.tmpl.lang.split( '-' ).join( '_' );
if ( wdLang === 'be_tarask' ) {
wdLang = 'be_x_old';
}

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


// Message with input links
// Message with input links
_ui.InputLinks = function( $Input, lang, title, entity ) {
_ui.InputLinks = ( $Input, lang, title, entity ) => {
var $LinksList = $( '<ul>' );
var $LinksList = $( '<ul>' );
Строка 1117: Строка 1116:


// Add a link to local page if it is available
// Add a link to local page if it is available
var thiswiki = entity.sitelinks && entity.sitelinks[ _c.wgDBname ];
var thiswiki = OO.getProp( entity, 'sitelinks', _c.wgDBname );
if ( thiswiki ) {
if ( thiswiki ) {
var $PageLink = $( '<a>' )
var $PageLink = $( '<a>' )
Строка 1155: Строка 1154:
if ( [ 'Enter', 'Space' ].includes( e.code ) ) {
if ( [ 'Enter', 'Space' ].includes( e.code ) ) {
e.preventDefault();
e.preventDefault();
$ClearInputLink.click();
this.click();
}
}
} )
} )
Строка 1165: Строка 1164:


// Load more button
// Load more button
_ui.LoadMore = function() {
_ui.LoadMore = () => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
flags: [ 'primary', 'progressive' ],
flags: [ 'primary', 'progressive' ],
Строка 1179: Строка 1178:


// Nearby page button (previous / next)
// Nearby page button (previous / next)
_ui.NearbyPage = function( type ) {
_ui.NearbyPage = ( type ) => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
disabled: true,
disabled: true,
Строка 1201: Строка 1200:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Dropdown,
IWRM.ui.Dropdown,
function( el ) {
( El ) => {
el.setValue( getPageTitle( index ) );
El.setValue( getPageTitle( index ) );
}
}
);
);
Строка 1214: Строка 1213:


// Page links (tabs and edit section)
// Page links (tabs and edit section)
_ui.PageLinks = function( title ) {
_ui.PageLinks = ( title ) => {
var $Element = $( '.mw-editsection' );
var $Element = $( '.mw-editsection' );
var accessKeyHtml = ( _isSiteWide ? '' : ' accesskey="c"');
var accessKeyHtml = ( _isSiteWide ? '' : ' accesskey="c"');
Строка 1296: Строка 1295:


// Random page button
// Random page button
_ui.RandomPage = function() {
_ui.RandomPage = () => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
disabled: true,
disabled: true,
Строка 1309: Строка 1308:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Dropdown,
IWRM.ui.Dropdown,
function( el ) {
( El ) => {
el.setValue( getPageTitle( rand ) );
El.setValue( getPageTitle( rand ) );
}
}
);
);
Строка 1322: Строка 1321:


// SiteSub
// SiteSub
_ui.SiteSub = function( count ) {
_ui.SiteSub = ( count ) => {
if ( typeof count === 'undefined' ) {
if ( typeof count === 'undefined' ) {
count = IWRM.data.changeCount;
count = IWRM.data.changeCount;
Строка 1359: Строка 1358:


// Skip a page
// Skip a page
_ui.SkipPage = function() {
_ui.SkipPage = () => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
disabled: true,
disabled: true,
Строка 1370: Строка 1369:


// Set event listeners
// Set event listeners
Btn.$button.on( 'click', function() {
Btn.$button.on( 'click', () => {
_ui.fn.removeArticle( IWRM.data.title );
_ui.fn.removeArticle( IWRM.data.title );
} );
} );
Строка 1378: Строка 1377:


// Submit changes
// Submit changes
_ui.Submit = function() {
_ui.Submit = () => {
var Btn = new OO.ui.ButtonInputWidget( {
var Btn = new OO.ui.ButtonInputWidget( {
disabled: true,
disabled: true,
Строка 1408: Строка 1407:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Summary,
IWRM.ui.Summary,
function( el ) {
( El ) => {
summary = el.getValue().trim();
summary = El.getValue().trim();
}
}
);
);
Строка 1428: Строка 1427:


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


Строка 1437: Строка 1436:


var textareaValue = Textarea.$input.data( 'old' );
var textareaValue = Textarea.$input.data( 'old' );
$FauxLine.find( '.iwrm-line input' ).each( function( index, el ) {
$FauxLine.find( '.iwrm-line input' ).each( ( index, el ) => {
$( el ).attr( 'disabled', value );
$( el ).attr( 'disabled', value );
if ( value === true ) {
if ( value === true ) {
Строка 1464: Строка 1463:


// Upper toolbar
// Upper toolbar
_ui.Toolbar = function() {
_ui.Toolbar = () => {
// Should be loaded first and foremost
// Should be loaded first and foremost
IWRM.ui.Dropdown = _ui.Dropdown();
IWRM.ui.Dropdown = _ui.Dropdown();
Строка 1491: Строка 1490:


// Check if article exists
// Check if article exists
_ui.fn.checkArticle = function( value, Field ) {
_ui.fn.checkArticle = ( value, Field ) => {
var match = _regularTmpl.exec( value );
var match = _regularTmpl.exec( value );
var title = match && match[ 1 ];
var title = match && match[ 1 ];
Строка 1498: Строка 1497:
function resolvedWithSuccess( data ) {
function resolvedWithSuccess( data ) {
var entity;
var entity;
var isMissing = data && data.entities && data.entities[ '-1' ];
var isMissing = OO.getProp( data, 'entities', '-1' );


if ( !isMissing ) {
if ( !isMissing ) {
Строка 1514: Строка 1513:


if ( title ) {
if ( title ) {
// Normalise language prefix for Wikidata
var wdLang = _c.wgContentLanguage.split( '-' ).join( '_' );
if ( wdLang === 'be_tarask' ) {
wdLang = 'be_x_old';
}

IWRM.api.checkSitelinks(
IWRM.api.checkSitelinks(
_c.wgContentLanguage,
wdLang,
title
title
).done( resolvedWithSuccess );
).done( resolvedWithSuccess );
Строка 1529: Строка 1522:


// Check nearby pages on validity
// Check nearby pages on validity
_ui.fn.checkBtns = function() {
_ui.fn.checkBtns = () => {
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.PreviousPage,
IWRM.ui.PreviousPage,
function( el ) {
( El ) => {
el.setDisabled( IWRM.data.index === 0 );
El.setDisabled( IWRM.data.index === 0 );
}
}
);
);
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.NextPage,
IWRM.ui.NextPage,
function( el ) {
( El ) => {
el.setDisabled( IWRM.data.index === IWRM.list.length - 1 );
El.setDisabled( IWRM.data.index === IWRM.list.length - 1 );
}
}
);
);
Строка 1546: Строка 1539:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.RandomPage,
IWRM.ui.RandomPage,
function( el ) {
( El ) => {
el.setDisabled( IWRM.list.length === 1 );
El.setDisabled( IWRM.list.length === 1 );
}
}
);
);
Строка 1553: Строка 1546:


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


Строка 1585: Строка 1578:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Submit,
IWRM.ui.Submit,
function( el ) {
( El ) => {
var val = typeof submitValue === 'undefined' ? value : submitValue;
var val = typeof submitValue === 'undefined' ? value : submitValue;
el.setDisabled( val );
El.setDisabled( val );
}
}
);
);
Строка 1593: Строка 1586:


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


IWRM.api.getSearchData( IWRM.offset + IWRM.prefs.limit ).then( function() {
IWRM.api.getSearchData().then( () => {
IWRM.offset += IWRM.prefs.limit;

// Update data in dropdown
// Update data in dropdown
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Dropdown,
IWRM.ui.Dropdown,
function( el ) {
( El ) => {
el.setOptions( IWRM.list );
El.setOptions( IWRM.list );
}
}
);
);
Строка 1616: Строка 1607:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.LoadMore,
IWRM.ui.LoadMore,
function( el ) {
( El ) => {
el.setDisabled( false );
El.setDisabled( false );
}
}
);
);
Строка 1627: Строка 1618:


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


// Find a future container for inputs
// Find a future container for inputs
Строка 1651: Строка 1643:


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


Строка 1664: Строка 1656:


// Append them all at once synchronously
// Append them all at once synchronously
var promises = Promise.all( InputFields ).then( function( Element ) {
var promises = Promise.all( InputFields ).then( ( Element ) => {
$InputHolder.append( Element );
$InputHolder.append( Element );
} ).then( function() {
} ).then( () => {
// Remove progress bar and show the diff
// Remove progress bar and show the diff
if ( index === length - 1 ) {
if ( index === length - 1 ) {
Строка 1705: Строка 1697:


return promises;
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 );
}
}


Строка 1714: Строка 1766:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Submit,
IWRM.ui.Submit,
function( element ) {
( El ) => {
element.$button.click();
El.$button.click();
_notification = _ui.fn.notify( null, null, {
_notification = _ui.fn.notify( null, null, {
text: _locale.errors.saving.replace( '$1', IWRM.data.title ),
text: _locale.errors.saving.replace( '$1', IWRM.data.title ),
Строка 1765: Строка 1817:
ifUIElementsExist(
ifUIElementsExist(
inputs[ nextIndex ],
inputs[ nextIndex ],
function( el ) {
( El ) => {
el.focus();
El.focus();
},
},
notifyIfNoNextInput
notifyIfNoNextInput
Строка 1774: Строка 1826:


// Parse changes in the diff view
// Parse changes in the diff view
_ui.fn.parseChanges = function( content ) {
_ui.fn.parseChanges = ( content ) => {
var result = content;
var result = content;
var hasErrors = false;
var hasErrors = false;
Строка 1781: Строка 1833:
+ '.iwrm-line input:not(:disabled)'
+ '.iwrm-line input:not(:disabled)'
);
);
$Changes.each( function( index, el ) {
$Changes.each( ( index, el ) => {
var old = $( el ).data( 'old' );
var old = $( el ).data( 'old' );
var value = $( el ).val();
var value = $( el ).val();
Строка 1819: Строка 1871:


// Remove an article from the lists
// Remove an article from the lists
_ui.fn.removeArticle = function( title ) {
_ui.fn.removeArticle = ( title ) => {
// Calculate the index and update the lists
// Calculate the index and update the lists
var index = getPageIndex( title );
var index = getPageIndex( title );
Строка 1832: Строка 1884:
ifUIElementsExist(
ifUIElementsExist(
IWRM.ui.Dropdown,
IWRM.ui.Dropdown,
function( el ) {
( El ) => {
el.setOptionsData( IWRM.list );
El.setOptionsData( IWRM.list );
el.setValue( getPageTitle( newIndex ) );
El.setValue( getPageTitle( newIndex ) );
IWRM.data.index = newIndex;
IWRM.data.index = newIndex;
}
}
Строка 1841: Строка 1893:


// Render a diff
// Render a diff
_ui.fn.renderDiff = function( title ) {
_ui.fn.renderDiff = ( title ) => {
var $Container = $( '.iwrm-diff' );
var $Container = $( '.iwrm-diff' );
var $DiffBar = new OO.ui.ProgressBarWidget( {
var $DiffBar = new OO.ui.ProgressBarWidget( {
Строка 1851: Строка 1903:
$Container.html( $DiffBar.$element );
$Container.html( $DiffBar.$element );


return IWRM.api.getFullText( title ).then( function() {
return IWRM.api.getFullText( title ).then( () => {
return IWRM.api.loadDiff( title ).then( function( data ) {
return IWRM.api.loadDiff( title ).then( ( data ) => {
// Insert the diff or the lack of it
// Insert the diff or the lack of it
var DiffResult = data && data.compare && data.compare.body;
var DiffResult = OO.getProp( data, 'compare', 'body' );
var Diff = _ui.Diff( DiffResult );
var Diff = _ui.Diff( DiffResult );
$Container.append( Diff );
$Container.append( Diff );
Строка 1870: Строка 1922:
IWRM.data.modifiedText = '';
IWRM.data.modifiedText = '';
} );
} );
} ).catch( function() {
} ).catch( () => {
// Remove progress bar and show the message
// Remove progress bar and show the message
$DiffBar.$element.remove();
$DiffBar.$element.remove();
Строка 1878: Строка 1930:


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


// Add a default text
// Add a default text
Строка 1908: Строка 1960:
autoHide: ( error ? false : true ),
autoHide: ( error ? false : true ),
title: ( error ? _locale.errors.title : '' ),
title: ( error ? _locale.errors.title : '' ),
tag: options.tag || null
tag: ( options && options.tag ? options.tag : null )
} );
} );
}
}
Строка 1915: Строка 1967:
* Initialising a portlet
* Initialising a portlet
*/
*/
IWRM.InitPortlet = function() {
IWRM.InitPortlet = () => {
var namespaces = [
if ( _c.wgNamespaceNumber !== 0 && _c.wgTitle !== 'Песочница' || mw.config.get( 'wgIsMainPage' ) ) {
0,
14, // Category
]
if ( !namespaces.includes( _c.wgNamespaceNumber ) && _c.wgTitle !== 'Песочница' || mw.config.get( 'wgIsMainPage' ) ) {
return;
return;
}
}
Строка 1924: Строка 1980:
}
}


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


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


mw.loader.using( 'mediawiki.util', function() {
mw.loader.using( 'mediawiki.util', () => {
var portlet = mw.util.addPortletLink(
var portlet = mw.util.addPortletLink(
'p-tb',
'p-tb',
Строка 1947: Строка 2003:
* Initialising
* Initialising
*/
*/
IWRM.Init = function() {
IWRM.Init = ( customList ) => {
// Check hash and user groups
// Check hash and user groups
var from = window.location.hash;
var from = window.location.hash;
Строка 1975: Строка 2031:
_ContentText.empty();
_ContentText.empty();
_Indicators.empty();
_Indicators.empty();
$( '.iwrm' ).removeClass( IWRM.prefs.loaded );


mw.loader.using( _deps ).done( function() {
mw.loader.using( _deps ).done( () => {
_api = new mw.Api();
_api = new mw.Api();
_wikidataApi = new mw.ForeignApi( 'https://www.wikidata.org/ruwiki/w/api.php' );
_wikidataApi = new mw.ForeignApi( 'https://www.wikidata.org/ruwiki/w/api.php' );
Строка 1991: Строка 2048:
} );
} );


IWRM.api.getSearchData().then( function() {
IWRM.api.getSearchData( customList ).then( () => {
// Render toolbar once
// Render toolbar once
_ContentText.append( _ui.Toolbar().$element );
_ContentText.append( _ui.Toolbar().$element );
Строка 2001: Строка 2058:
_ContentText.append( _ui.Footer().$element );
_ContentText.append( _ui.Footer().$element );


// Remove progress bar and show the page
// Actually render diff
_ui.fn.renderDiff( IWRM.data.title ).then( function() {
_ui.fn.renderDiff( IWRM.data.title ).then( () => {
// Remove progress bar and show the page
$ContentBar.$element.remove();
$ContentBar.$element.remove();
_ContentText.addClass( IWRM.prefs.loaded );
_ContentText.addClass( IWRM.prefs.loaded );
Строка 2010: Строка 2066:
// Confirm before leaving with a hash
// Confirm before leaving with a hash
mw.confirmCloseWindow( {
mw.confirmCloseWindow( {
test: function() {
test: () => {
return window.location.hash.startsWith( IWRM.prefs.hash );
return window.location.hash.startsWith( IWRM.prefs.hash );
}
}
Строка 2016: Строка 2072:


// Remove notifications on saving
// Remove notifications on saving
mw.hook( 'postEdit' ).add( function() {
mw.hook( 'postEdit' ).add( () => {
if ( _notification ) {
if ( _notification ) {
_notification = _notification.close();
_notification = _notification.close();
Строка 2022: Строка 2078:
} );
} );
} );
} );
} ).fail( function( error, data ) {
} ).fail( ( error, data ) => {
_ui.fn.notify( error, data, { replace: _locale.requests.init } );
_ui.fn.notify( error, data, { replace: _locale.requests.init } );
} );
} );

Текущая версия от 16:31, 11 ноября 2024

/* <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/ruwiki/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();
	}
}() );