User:Ingenuity/combined.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* Evad37/OneClickArchiver.js */
/**
 * Derived from Technical 13's version [1] of Equazcion's OneClickArchiver [2]
 * [1] < https://en.wikipedia.org/wiki/User:Technical_13/Scripts/OneClickArchiver.js >
 * [2] < https://en.wikipedia.org/wiki/User:Equazcion/OneClickArchiver.js >
 */
// <nowiki>
mw.loader.using(['mediawiki.util', 'mediawiki.api'], function() {
var config = mw.config.get([
'debug',
'wgAction',
'wgArticleId',
'wgCategories',
'wgMonthNames',
'wgNamespaceNumber',
'wgPageName',
'wgRelevantUserName'
]);
$( document ).ready( function () {
if ( ( $( '#ca-addsection' ).length > 0 ||
$.inArray( 'Non-talk pages that are automatically signed', config.wgCategories )  >= 0 ) &&
config.wgAction === 'view' &&
$.inArray( 'Pages that should not be manually archived', config.wgCategories ) === -1 ) {
var OCAstate = mw.user.options.get( 'userjs-OCA-enabled', 'true' );
var pageid = config.wgArticleId;
var errorLog = { errorCount: 0 };
new mw.Api().get( {
action: 'query',
prop: [ 'revisions', 'info' ],
rvsection: 0,
rvprop: 'content',
pageids: pageid,
indexpageids: 1,
rawcontinue: ''
} ).done( function ( response0 ) {
var thisMonthNum, thisMonthFullName, monthNamesShort, thisMonthShortName, thisYear, archiveNum;
var content0 = response0.query.pages[ pageid ].revisions[ 0 ][ '*' ];
 
/* archiveme */// Find out if there is already an {{Archive me}} request, and if it is between 1-2 months old
if ( config.wgNamespaceNumber === 3 ) {
thisMonthNum = new Date().getMonth();
thisMonthFullName = config.wgMonthNames[ thisMonthNum + 1 ];
monthNamesShort = [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
thisMonthShortName = monthNamesShort[ thisMonthNum + 1 ];
thisYear = new Date().getFullYear();
var nowOcto = parseInt( ( ( thisYear * 12 ) + thisMonthNum + 1 ), 10 );
var archiveme = content0.match( /\{\{Archive ?me(\| *date *= *(January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}))?\}\}/i );
if ( archiveme === null || archiveme === undefined ) {
errorLog.errorCount++;
errorLog.archiveme = '{{Archiveme}} not found.';
} else {
/* Archive me found - how old is it? */
var archivemeMonth = archiveme[ 2 ];
var archivemeMonthNum = 0;
if ( typeof archivemeMonth === 'number' ) {
archivemeMonthNum = parseInt( archivemeMonth, 10 );
} else {
for ( var i in config.wgMonthNames ) {
if ( archivemeMonth === config.wgMonthNames[ i ] ) {
archivemeMonthNum = parseInt( i, 10 );
} else if ( archivemeMonth === monthNamesShort[ i ] ) {
archivemeMonthNum = parseInt( i, 10 );
}
}
}
var archivemeYear = parseInt( archiveme[ 3 ], 10 );
var archivemeOcto = parseInt( ( ( archivemeYear * 12 ) + archivemeMonthNum ), 10 );
var archivemeSafe = parseInt( ( nowOcto - 2 ), 10 );
archiveme = archiveme[ 0 ];
}
}
 
/* counter */// Get the counter value
var counterRegEx = new RegExp( '\\| *counter *= *(\\d+)' );
var counter = counterRegEx.exec( content0 );
if ( counter === null || counter === undefined ) {
counter = 1;
errorLog.errorCount++;
errorLog.counter = counter;
} else {
counter = counter[ 1 ];
archiveNum = counter;
}
 
/* archiveName */// Get the archiveName value
var archiveNameRegEx = /\| *archive *= *(.*\%\(counter\)d.*) *(-->)?/;
var archiveName = archiveNameRegEx.exec( content0 );
var rootBase = config.wgPageName
.replace( /\/.*/g, '' )// Chop off the subpages
.replace( /_/g, ' ' );// Replace underscores with spaces
if ( archiveName === null || archiveName === undefined ) {
archiveName = rootBase + '/Archive ' + counter;
errorLog.errorCount++;
errorLog.archiveName = archiveName;
} else { 
archiveName = archiveName[ 1 ]
.replace( /\| *archive *= */, '' )
.replace( /\%\(year\)d/g, thisYear )
.replace( /\%\(month\)d/g, thisMonthNum )
.replace( /\%\(monthname\)s/g, thisMonthFullName )
.replace( /\%\(monthnameshort\)s/g, thisMonthShortName )
.replace( /\%\(counter\)d/g, archiveNum );
var archiveBase = archiveName
.replace( /\/.*/, '' )// Chop off the subpages
.replace( /_/g, ' ' );// Replace underscores with spaces
var archiveSub = archiveName
.replace( /_/g, ' ' )// Replace underscores with spaces
.replace( archiveBase, '' );// Chop off the base pagename
if ( archiveBase != rootBase ) {
errorLog.errorCount++;
errorLog.archiveName = 'Archive name mismatch:<br /><br />Found: ' + archiveName;
errorLog.archiveName += '<br />Expected: ' + rootBase.replace( '_', ' ' ) + archiveSub + '<br /><br />';
}
}
 
/* archivepagesize */// Get the size of the destination archive from the API
new mw.Api().get( {
action: 'query',
prop: 'revisions',rvlimit: 1,
rvprop: [ 'size', 'content' ],
titles: archiveName,
list: 'usercontribs',
uclimit: 1,
ucprop: 'timestamp',
ucuser: ( ( config.wgRelevantUserName ) ?
config.wgRelevantUserName : 'Example' ),
rawcontinue: '',
} ).done( function ( archivePageData ) {
var archivePageSize = 0;
if ( archivePageData.query.pages[ -1 ] === undefined ) {
for ( var a in archivePageData.query.pages ) {
archivePageSize = parseInt( archivePageData.query.pages[ a ].revisions[ 0 ].size, 10 );
archiveName = archivePageData.query.pages[ a ].title;
}
} else {
archivePageSize = -1;
archiveName = archivePageData.query.pages[ archivePageSize ].title;
errorLog.errorCount++;
errorLog.archivePageSize = -1;
errorLog.archiveName = '<a class="new" href="' +
mw.util.getUrl( archiveName, { action: 'edit', redlink: '1' } ) +
'" title="' + archiveName + '">' + archiveName + '</a>';
}
 
/* maxarchivesize */// Get the defined max archive size from template
var maxArchiveSizeRegEx = new RegExp( '\\| *maxarchivesize *= *(\\d+K?)' );
var maxArchiveSize = maxArchiveSizeRegEx.exec( content0 );
if ( maxArchiveSize === null || maxArchiveSize[ 1 ] === undefined ) {
maxArchiveSize = parseInt( 153600, 10 );
errorLog.errorCount++;
errorLog.maxArchiveSize = maxArchiveSize;
} else if ( maxArchiveSize[ 1 ].slice( -1 ) === "K" && $.isNumeric( maxArchiveSize[ 1 ].slice( 0, maxArchiveSize[ 1 ].length-1 ) ) ) {
maxArchiveSize = parseInt( maxArchiveSize[ 1 ].slice( 0, maxArchiveSize[ 1 ].length - 1 ), 10 ) * 1024;
} else if ( $.isNumeric( maxArchiveSize[ 1 ].slice() ) ) {
maxArchiveSize = parseInt( maxArchiveSize[ 1 ].slice(), 10 );
}
 
/* pslimit */// If maxArchiveSize is defined, and archivePageSize >= maxArchiveSize increment counter and redfine page name.
if ( !errorLog.maxArchiveSize && archivePageSize >= maxArchiveSize ) {
counter++;
archiveName = archiveNameRegEx.exec( content0 );
archiveName = archiveName[ 1 ]
.replace( /\| *archive *= */, '' )
.replace( /\%\(year\)d/g, thisYear )
.replace( /\%\(month\)d/g, thisMonthNum )
.replace( /\%\(monthname\)s/g, thisMonthFullName )
.replace( /\%\(monthnameshort\)s/g, thisMonthShortName )
.replace( /\%\(counter\)d/g, counter );
var oldCounter = counterRegEx.exec( content0 );
var newCounter = '|counter=1';
if ( oldCounter !== null && oldCounter !== undefined ) {
newCounter = oldCounter[ 0 ].replace( oldCounter[ 1 ], counter );
oldCounter = oldCounter[ 0 ];
} else {
errorLog.errorCount++;
errorLog.newCounter = newCounter;
}
}
 
/* archiveheader */// Get the defined archive header to place on archive page if it doesn't exist
var archiveHeaderRegEx = new RegExp( '\\| *archiveheader *= *(\{\{[^\r\n]*\}\})' );
var archiveHeader = archiveHeaderRegEx.exec( content0 );
if ( archiveHeader === null || archiveHeader === undefined ) {
archiveHeader = "{{Aan}}";
errorLog.errorCount++;
errorLog.archiveHeader = archiveHeader;
} else {
archiveHeader = archiveHeader[ 1 ];
}
 
/* headerlevel */// Get the headerlevel value or default to '2'
var headerLevelRegEx = new RegExp( '\\| *headerlevel *= *(\\d+)' );
var headerLevel = headerLevelRegEx.exec( content0 );
if ( headerLevel === null || headerLevel === undefined ) {
headerLevel = 2;
errorLog.errorCount++;
errorLog.headerLevel = headerLevel;
} else {
headerLevel = parseInt( headerLevel[ 1 ] );
}
 
/* debug */// Table to report the values found.
if ( config.debug === true ) {
var OCAreport = '<table style="width: 100%;" border="1"><tr><th style="font-variant: small-caps; font-size: 20px;">config</th><th style="font-variant: small-caps; font-size: 20px;">value</th></tr>';
OCAreport += '<tr><td>Counter</td><td style="text-align: center;';
if ( errorLog.counter ) { OCAreport += ' background-color: #FFEEEE;">' + errorLog.counter; }
else { OCAreport += '">' + counter; }
OCAreport += '</td></tr><tr><td colspan="2" style="text-align: center;">Archive name</td></tr><tr><td colspan="2" style="text-align: center;';
if ( errorLog.archiveName ) { OCAreport += ' background-color: #FFEEEE;">' + errorLog.archiveName; }
else { OCAreport += '">' + archiveName; }
OCAreport += '</td></tr><tr><td>Header Level</td><td style="text-align: center;';
if ( errorLog.headerLevel ) { OCAreport += ' background-color: #FFEEEE;">' + errorLog.headerLevel; }
else { OCAreport += '">' + headerLevel; }
OCAreport +=  '</td></tr><tr><td>Archive header</td><td style="text-align: center;';
if ( errorLog.archiveHeader ) { OCAreport += ' background-color: #FFEEEE;">' + errorLog.archiveHeader; }
else { OCAreport += '">' + archiveHeader; }
OCAreport +=  '</td></tr><tr><td>Max<br />archive size</td><td style="text-align: center;';
if ( errorLog.maxArchiveSize ) { OCAreport += ' background-color: #FFEEEE;">' + errorLog.maxArchiveSize; }
else { OCAreport += '">' + maxArchiveSize; }
OCAreport +=  '</td></tr><tr><td>Current<br />archive size</td><td style="text-align: center;';
if ( errorLog.archivePageSize ) { OCAreport += ' background-color: #FFEEEE;">' + archivePageSize; }
else if ( archivePageSize >= maxArchiveSize ) { OCAreport += ' background-color: #FFEEEE;">' + archivePageSize; }
else { OCAreport += '">' + archivePageSize; }
if ( !errorLog.archiveme && archiveme !== undefined ) {
OCAreport +=  '</td></tr><tr><td colspan="2" style="text-align: center;';
if ( ( nowOcto - archivemeOcto ) <= 1 ) { OCAreport += '">Asked to archive '; }
if ( ( nowOcto - archivemeOcto ) === 0 ) { OCAreport += 'this month'; }
else if ( ( nowOcto - archivemeOcto ) === 1 ) { OCAreport += 'last month'; }
else { OCAreport += ' background-color: #FFEEEE;">Asked to archive ' + ( nowOcto - archivemeOcto ) + ' months ago'; }
}
if ( errorLog.archiveme || archiveme !== undefined ) { OCAreport +=  '</td></tr><tr><td colspan="2" style="text-align: center;'; }
if ( errorLog.archiveme ) { OCAreport +=  ' background-color: #FFEEEE;">' + errorLog.archiveme; }
else if ( archiveme !== undefined ) { OCAreport += '">' + archiveme; }
OCAreport +=  '</td></tr><tr><td colspan="2" style="font-size: larger; text-align: center;"><a href="/wiki/User:Technical_13/Scripts/OneClickArchiver" title="User:Technical 13/Scripts/OneClickArchiver">Documentation</a></td></tr></table>';
mw.notify( $( OCAreport ), { title: 'OneClickArchiver report!', tag: 'OCA', autoHide: false } );
}
 
var OCAerror = '<p>The following errors detected:<br />';
if ( errorLog.counter ) { OCAerror += '<b style="font-size: larger; color: #FF0000;">&bull;</b>&nbsp;Unable to find <b>|counter=</b><br />&nbsp; &nbsp; &nbsp;Default value: <b>1</b><br />'; }
if ( errorLog.archiveName && errorLog.archiveName.search( 'defaulted to' ) !== -1 ) { OCAerror += '<b style="font-size: larger; color: #FF0000;">&bull;</b>&nbsp;Unable to find <b>|archive=</b><br />&nbsp; &nbsp; &nbsp;Default value: <b>' + archiveName + '</b><br />'; }
if ( errorLog.archiveName && errorLog.archiveName.search( 'mismatch' ) !== -1 ) { OCAerror += '<b style="font-size: larger; color: #FF0000;">&bull;</b>&nbsp;Archive name mismatch detected.<br />'; }
if ( errorLog.headerLevel ) { OCAerror += '&nbsp; Unable to find <b>|headerlevel=</b><br />&nbsp; &nbsp; &nbsp;Default value: <b>2</b><br />'; }
if ( errorLog.archiveHeader ) { OCAerror += '&nbsp; Unable to find <b>|archiveheader=</b><br />&nbsp; &nbsp; &nbsp;Default value: <b>"{{Aan}}"</b><br />'; }
if ( errorLog.maxArchiveSize ) { OCAerror += '&nbsp; Unable to find <b>|maxarchivesize=</b><br />&nbsp; &nbsp; &nbsp;Default value: <b>153600</b><br />'; }
if ( errorLog.counter || errorLog.archiveName ) { OCAerror += '<br /><b style="font-size: larger; color: #FF0000;">&bull;</b>&nbsp;Causing the script to abort.<br />'; }
OCAerror += '<br /><span style="font-size: larger;">Please, see <a href="/wiki/User:Technical_13/Scripts/OneClickArchiver" title="User:Technical 13/Scripts/OneClickArchiver">the documentation</a> for details.</span></p>';
var archiverReport = mw.util.addPortletLink(
'p-cactions',
'#archiverNoLink',
'|Archive',
'pt-OCA-report',
'Report for why there are no |Archive links on this page',
null,
null
);
$( archiverReport ).click( function ( e ) {
e.preventDefault();
mw.notify( $( OCAerror ), { title: 'OneClickArchiver errors!', tag: 'OCAerr', autoHide: false } );
} );
 
if ( config.wgNamespaceNumber === 3 && ( errorLog.counter || errorLog.archiveName ) &&
config.debug === true && errorLog.archiveme )  {
if ( confirm( 'Click [OK] to post {{Archiveme|{{SUBST:DATE}}}} to the top of the page and abort or\n\t[Cancel] to attempt running with default values.' ) === true ) {
new mw.Api().postWithToken( 'edit', {
action: 'edit',
section: 0,
pageid: pageid,
text: '{{Archiveme|{{SUBST:DATE}}}}\n' + content0,
summary: '{{[[Template:Archiveme|Archiveme]]}} posted with [[User:Evad37/OneClickArchiver|OneClickArchiver]].'
} ).done( function () {
alert( 'Request for user to set up archiving posted.' );
location.reload();
} );
}
} else if ( config.wgNamespaceNumber === 3 && archivemeOcto >= archivemeSafe ) {
 
/* Archive me request was made, give the user a chance to comply */
 
} else if ( config.wgNamespaceNumber === 3 && ( errorLog.counter || errorLog.archiveName ) && config.debug === true && confirm( '{{Archiveme}} found on the top of the page:\n\n\t Click [OK] abort or\n\t[Cancel] to attempt running with default values.' ) === true ) {
 
/* User aborted script */
 
} else {
$( 'h' + headerLevel + ' span.mw-headline' ).each( function() {
var sectionName = $( this ).text();
var editSectionUrl = $( this ).parent().find('.mw-editsection a').not('.mw-editsection-visualeditor').first().attr( 'href' );
var sectionReg = /&section=(.*)/;
var sectionRaw = sectionReg.exec( editSectionUrl );
if ( sectionRaw != null && sectionRaw[ 1 ].indexOf( 'T' ) < 0 ) {
var sectionNumber = parseInt( sectionRaw[ 1 ] );
if ( $( this ).parent().prop( 'tagName' ) === 'H' + headerLevel ) {
 
$( this ).parent( 'h' + headerLevel ).append(
' <div style="font-size: 0.6em; font-weight: bold; float: right;"> | <a id="' + sectionNumber +
'" href="#archiverLink" class="archiverLink">' + 'Archive' + '</a></div>'
);
$(this).parent('h' + headerLevel).find('a.archiverLink').attr('title', 'Archive to: "'+archiveName+'"');
$( this ).parent( 'h' + headerLevel ).find( 'a.archiverLink' ).click( function() {
 
var mHeaders = '<span style="color: #444444;">Retrieving headers...</span>';
var mSection = 'retrieving section content...';
var mPosting = '<span style="color: #004400">Content retrieved,</span> performing edits...';
var mPosted = '<span style="color: #008800">Archive appended...</span>';
var mCleared = '<span style="color: #008800">Section cleared...</span>';
var mReloading = '<span style="color: #000088">All done! </span><a href="#archiverLink" onClick="javascript:location.reload();" title="Reload page">Reloading</a>...';
 
$( 'body' ).append( '<div class="overlay" style="background-color: #000000; opacity: 0.4; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 500;"></div>' );
 
$( 'body' ).prepend( '<div class="arcProg" style="font-weight: bold; box-shadow: 7px 7px 5px #000000; font-size: 0.9em; line-height: 1.5em; z-index: 501; opacity: 1; position: fixed; width: 50%; left: 25%; top: 30%; background: #F7F7F7; border: #222222 ridge 1px; padding: 20px;"></div>' );
 
$( '.arcProg' ).append( '<div>' + mHeaders + '</div>' );
 
$( '.arcProg' ).append( '<div>' + 'Archive name <span style="font-weight: normal; color: #003366;">' + archiveName + '</span> <span style="color: darkgreen;">found</span>, ' + mSection + ' (' + archivePageSize + 'b)</div>' );
new mw.Api().get( {
action: 'query',
pageids: pageid,
rvsection: sectionNumber,
prop: [ 'revisions', 'info' ],
rvprop: 'content',
indexpageids: 1,
rawcontinue: ''
} ).done( function ( responseSection ) {
var sectionContent = responseSection.query.pages[ pageid ].revisions[ 0 ][ '*' ];
$( '.arcProg' ).append( '<div>' + mPosting + '</div>' );
 
var dnau = sectionContent.match( /<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}), ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/ );
var dnauDate;
if ( dnau === null || dnau === undefined ) {
dnauDate = Date.now();
dnau = null;
} else {
dnau = dnau[ 1 ] + ':' + dnau[ 2 ] + ' ' + dnau[ 3 ] + ' ' + dnau[ 4 ] + ' ' + dnau[ 5 ];
dnauDate = new Date( dnau );
dnauDate = dnauDate.valueOf();
}
 
if ( dnauDate > Date.now() ) {
$( '.arcProg' ).remove();
$( '.overlay' ).remove();
var dnauAbortMsg = '<p>This section has been marked \"Do Not Archive Until\" ' + dnau + ', so archiving was aborted.<br /><br /><span style="font-size: larger;">Please, see <a href="/wiki/User:Technical_13/Scripts/OneClickArchiver" title="User:Technical 13/Scripts/OneClickArchiver">the documentation</a> for details.</span></p>';
mw.notify( $( dnauAbortMsg ), { title: 'OneClickArchiver aborted!', tag: 'OCAdnau', autoHide: false } );
} else {
var archiveAction = 'adding section';
if ( archivePageSize <= 0 || ( archivePageSize >= maxArchiveSize && !errorLog.maxArchiveSize ) ) {
sectionContent = archiveHeader + '\n\n' + sectionContent;
archiveAction = 'creating';
mPosted = '<span style="color: #008800">Archive created...</span>';
} else {
sectionContent = '\n\n{{Clear}}\n' + sectionContent;
}
 
if ( dnau != null ) {
sectionContent = sectionContent.replace( /<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}), ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/g, '' );
}
 
new mw.Api().postWithToken( 'edit', {
action: 'edit',
title: archiveName,
appendtext: sectionContent,
summary: '/* '+sectionName+' */ archived using [[User:Evad37/OneClickArchiver|OneClickArchiver]])'
} ).done( function () {
$( '.arcProg' ).append( '<div class="archiverPosted">' + mPosted + '</div>' );
new mw.Api().postWithToken( 'edit', {
action: 'edit',
section: sectionNumber,
pageid: pageid,
text: '',
summary: '[[User:Evad37/OneClickArchiver|OneClickArchived]] "' + sectionName + '" to [[' + archiveName + ']]'
} ).done( function () {
$( '.arcProg' ).append( '<div class="archiverCleared">' + mCleared + '</div>' );
if ( archivePageSize >= maxArchiveSize && !errorLog.maxArchiveSize ) {
var mUpdated = '<span style="color: #008800">Counter updated...</span>';
new mw.Api().postWithToken( 'edit', {
action: 'edit',
section: 0,
pageid: pageid,
text: content0.replace( oldCounter, newCounter ),
summary: '[[User:Evad37/OneClickArchiver|OneClickArchiver]] updating counter.'
} ).done( function () {
$( '.arcProg' ).append( '<div class="archiverPosted">' + mUpdated + '</div>' );
$( '.arcProg' ).append( '<div>' + mReloading + '</div>' );
location.reload();
} );
} else {
$( '.arcProg' ).append( '<div>' + mReloading + '</div>' );
location.reload();
}
} );
} );
}
} );
} );
}
}
} );
}
} );
} );
var linkTextD = '1CA is on', linkDescD = 'Disable OneClickArchiver';
var linkTextE = '1CA is off', linkDescE = 'Enable OneClickArchiver';
var linkText = linkTextD, linkDesc = linkDescD;
if ( OCAstate === 'false' ) {
linkText = linkTextE; linkDesc = linkDescE;
$( 'div.archiverDiv, li#pt-OCA-report' ).css( 'display', 'none' );
}
var archiverToggle = mw.util.addPortletLink(
'p-cactions',
'#archiverLink',
linkText,
'pt-OCA',
linkDesc,
'o',
null
);
$( archiverToggle ).click( function ( e ) {
e.preventDefault();
/* Toggle the archiveLinks */
$( 'div.archiverDiv' ).css( 'display', function ( _i, val ) {
return val === 'none' ? '' : 'none';
});
/* Toggle the toggle link */
$( 'li#pt-OCA a' ).html( function ( _i, val ) {
return val === linkTextD ? linkTextE : linkTextD;
});
/* Toggle the toggle description */
$( 'li#pt-OCA a' ).attr( 'title', function ( _i, val ) {
return val === linkDescD ? linkDescE : linkDescD;
});
/* Toggle the error report link */
if ( ( errorLog.counter || errorLog.archiveName ) ) {
$( 'li#pt-OCA-report' ).css( 'display', function ( _i, val ) {
return val === 'none' ? '' : 'none';
});
}
/* Toggle default state */
new mw.Api().postWithToken( 'options', {
action: 'options',
optionname: 'userjs-OCA-enabled',
optionvalue: OCAstate === 'true' ? 'false' : 'true'
} ).done( function() {
var resultMsg = 'OneClickArchiver is now ' + ( OCAstate === 'true' ? 'disabled' : 'enabled' ) + ' by default.';
mw.notify(resultMsg);
OCAstate = OCAstate === 'true' ? 'false' : 'true';
} );
} );
}
} );
});
// </nowiki>

/* Evad37/MoveToDraft.js */
/***************************************************************************************************
 MoveToDraft
-------------
Version 2.5.0
-------------
A script to move unsourced articles to draft space, including cleanup and author notification.
- Moves page to draftspace
- Checks if any files used are non-free
- Checks if any redirects pointed to the page
- Comments out non-free files, turn categories into links, add afc draft template, add redirects
- Adds notification message on author talk page
- Updates talk page banners
- Logs draftification in user subpage
***************************************************************************************************/
/* jshint laxbreak: true, undef: true, maxerr:999 */
/* globals console, window, document, $, mw, OO, extraJs */
// <nowiki>
$.when(
// Resource loader modules
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'ext.gadget.libExtraUtil']),
// Page ready
$.ready
).then(function() {
/* ========== Config ============================================================================ */
var config = {
// Script info
script: {
advert:  ' ([[User:Evad37/MoveToDraft.js|via script]])', // For edit summaries
version: '2.5.1'
},
// MediaWiki configuration values
mw: mw.config.get( [
'wgArticleId',
'wgCurRevisionId',
'wgPageName',
'wgUserGroups',
'wgUserName',
'wgMonthNames',
'wgNamespaceNumber',
'wgTitle'
] )
};
/* ========== API =============================================================================== */
var API = new mw.Api( {
ajax: {
headers: { 
'Api-User-Agent': 'MoveToDraft/' + config.script.version + 
' ( https://en.wikipedia.org/wiki/User:Evad37/MoveToDraft )'
}
}
} );
var moveToDraft = function moveToDraft() {
/* ========== Additional config ================================================================= */
// Wikitext strings
config.wikitext = {
'rationale':window.m2d_rationale || '[[WP:DRAFTIFY|Not ready]] for mainspace, incubate in draftspace',
'editsummary':window.m2d_editsummary || window.m2d_rationale || '[[WP:AFC|AFC]] draft',
'notification_heading': '[[Draft:$1|$1]] moved to draftspace',
'notification':window.m2d_notification || "An article you recently created, [[Draft:$1|$1]], is not suitable as written to remain published. It needs more citations from [[WP:RS|reliable]], [[WP:IS|independent sources]]. <small>([[WP:42|?]])</small> Information that can't be referenced should be removed ([[WP:V|verifiability]] is of [[WP:5|central importance]] on Wikipedia). I've moved your draft to [[Wikipedia:Draftspace|draftspace]] (with a prefix of \"<code>Draft:</code>\" before the article title) where you can incubate the article with minimal disruption. When you feel the article meets Wikipedia's [[WP:GNG|general notability guideline]] and thus is ready for mainspace, please click on the \"Submit your draft for review!\" button at the top of the page. ~~~~",
'logMsg':'#[[$1]] moved to [[$2]] at ~~~~~'
};
config.doNotLog = window.m2d_doNotLog ? true : false;
// Page data -- to be retreived later from api
config.pagedata = {};
// Helper functions
// - prettify an encoded page title (or at least replace underscores with spaces)
var getPageText = function(p) {
var t = mw.Title.newFromText( decodeURIComponent(p) );
if (t) {
return t.getPrefixedText();
} else {
return p.replace(/_/g, " ");
}
};

/* ========== Tasks ============================================================================= */
// Grab page data - initial author, current wikitext, any redirects, if Draft: page already exists
var grabPageData = function() {
var patt_isRedirect = /^\s*#redirect/i;
var checkedPageTriageStatus = false;
// Function to check if all done
var checkPageData = function() {
if (
config.pagedata.author != null &&
config.pagedata.oldwikitext != null &&
config.pagedata.redirects != null &&
checkedPageTriageStatus
) {
//all done - go to next screen
screen1();
}
};
/* ---------- Initial author ---------------------------------------------------------------- */
/* Try making an api call for just the first revision - but if that is a redirect, then get 'max'
number of revisions, and look for first non-redirect revision - use this as the initial author,
not the creator of the redirect.
*/
var processMaxRvAuthorQuery = function (result) {
var revisions = result.query.pages[config.mw.wgArticleId].revisions;
for ( var i=1; i<revisions.length; i++ ) {
if ( !patt_isRedirect.test(revisions[i]['*']) ) {
config.pagedata.author = revisions[i].user;
break;
}
}
//Check that we actually found an author (i.e. not all revisions were redirects
if ( config.pagedata.author == null ) {
API.abort();
var retry = confirm("Could not retrieve page author:\n"+extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
screen0();
} else {
$("#M2D-modal").remove();
}
}
checkPageData();
};
var processAuthorQuery = function (result) {
// Check if page is currently a redirect
if ( result.query.pages[config.mw.wgArticleId].redirect ) {
API.abort();
alert("Error: " + config.mw.wgPageName + " is a redirect");
return;
}
// Check if first revision is a redirect
rvwikitext = result.query.pages[config.mw.wgArticleId].revisions[0]['*'];
if ( patt_isRedirect.test(rvwikitext) ) {
// query to look for first non-redirect revision
API.get( {
action: 'query',
pageids: config.mw.wgArticleId,
prop: 'revisions',
rvprop: ['user', 'content'],
rvlimit: 'max',
rvdir: 'newer'
} )
.done( processMaxRvAuthorQuery )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
API.abort();
var retry = confirm("Could not retrieve page author:\n"+extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
screen0();
} else {
$("#M2D-modal").remove();
}
} );
return;
}
config.pagedata.author = result.query.pages[config.mw.wgArticleId].revisions[0].user;
checkPageData();
};
//Get author
API.get( {
action: 'query',
pageids: config.mw.wgArticleId,
prop: ['revisions', 'info'],
rvprop: ['user', 'content'],
rvlimit: 1,
rvdir: 'newer'
} )
.done( processAuthorQuery )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
API.abort();
var retry = confirm("Could not retrieve page author:\n"+extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
screen0();
} else {
$("#M2D-modal").remove();
}
} );
/* ---------- Current wikitext -------------------------------------------------------------- */
API.get( {
action: 'query',
pageids: config.mw.wgArticleId,
prop: 'revisions',
rvprop: 'content'
} )
.done( function(result) {
config.pagedata.oldwikitext = result.query.pages[config.mw.wgArticleId].revisions[0]['*'];
checkPageData();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
API.abort();
var retry = confirm("Could not retrieve page wikitext:\n"+ extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
screen0();
} else {
$("#M2D-modal").remove();
}
} );
//TODO(?): also get proposed Draft: page (to check if it is empty or not)
/* ---------- Redirects --------------------------------------------------------------------- */
var redirectTitles = [];
var processRedirectsQuery = function(result) {
if ( !result.query || !result.query.pages ) {
// No results
config.pagedata.redirects = false;
checkPageData();
return;
}
// Gather redirect titles into array
$.each(result.query.pages, function(_id, info) {
redirectTitles.push(info.title);
});
// Continue query if needed
if ( result.continue ) {
doRedirectsQuery($.extend(redirectsQuery, result.continue));
return;
}
// Check if redirects were found
if ( redirectTitles.length === 0 ) {
config.pagedata.redirects = false;
checkPageData();
return;
}
// Set redirects
config.pagedata.redirects = ( redirectTitles.length === 0 ) ? false : redirectTitles;
checkPageData();
};
var redirectsQuery = {
action: 'query',
pageids: config.mw.wgArticleId,
generator: 'redirects',
grdlimit: 500
};
var doRedirectsQuery = function(q) {
API.get( q )
.done( processRedirectsQuery )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
API.abort();
var retry = confirm("Could not retrieve redirects:\n" + extraJs.makeErrorMsg(c, r) +
"\n\nTry again? (or Cancel to skip)");
if ( retry ) {
screen0();
} else {
config.pagedata.redirects = false;
checkPageData();
}
} );
};
doRedirectsQuery(redirectsQuery);
/* ---------- Review (Page Triage) status ----------------------------------------------------------------- */
API.get( {
action: 'pagetriagelist',
page_id: config.mw.wgArticleId
} )
.done( function(result) {
if ( !result.pagetriagelist.pages.length ) {
var keepGoing = confirm('WARNING: Page has already been reviewed by a New Page Patroller. Are you sure you want to draftify this page?');
if ( !keepGoing ) {
API.abort();
$("#M2D-modal").remove();
return;
}
}
checkedPageTriageStatus = true;
checkPageData();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
API.abort();
var retry = confirm("Could not retrieve page triage status:\n"+ extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
screen0();
} else {
$("#M2D-modal").remove();
}
} );
};
//Move page
var movePage = function() {
$("#M2D-task0").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status0").html("...");
// First check the page hasn't been draftified in the meantime
API.get({
action: "query",
pageids: config.mw.wgArticleId,
format: "json",
formatversion: "2"
}).then(function(response) {
var page = response && response.query && response.query.pages && response.query.pages[0];
if (!page) {
return $.Deferred().reject();
} else if (page.missing) {
return $.Deferred().reject("moveToDraft-pagemissing");
} else if (page.ns === 118 /* Draft NS */) {
return $.Deferred().reject("moveToDraft-alreadydraft");
} else if (page.ns !== config.mw.wgNamespaceNumber) {
return $.Deferred().reject("moveToDraft-movednamespace");
}
return API.postWithToken( 'csrf', {
action: 'move',
fromid: config.mw.wgArticleId,
to: config.inputdata.newTitle,
movetalk: 1,
noredirect: 1,
reason: config.inputdata.rationale + config.script.advert
} );
})
.done( function() {
if (
-1 === $.inArray('sysop', config.mw.wgUserGroups) &&
-1 === $.inArray('extendedmover', config.mw.wgUserGroups)
) {
// Newly created redirect to be tagged for speedy deletion
tagRedrect();
return;
}
$("#M2D-task0").css({"color":"#000", "font-weight":""});
$("#M2D-status0").html("Done!");
getImageInfo();
} )
.fail( function(c,r) {
if ( r && r.textStatus === 'abort' ) {
return;
} else if (c === "moveToDraft-pagemissing") {
alert("The page no longer appears to exists. It may have been deleted.");
$("#M2D-modal").remove();
window.location.reload();
return;
} else if (c === "moveToDraft-alreadydraft") {
alert("Aborted: The page has already been moved to draftspace.");
$("#M2D-modal").remove();
window.location.reload();
return;
} else if (c === "moveToDraft-alreadydraft") {
alert("Aborted: The page has already been moved out of mainspace.");
$("#M2D-modal").remove();
window.location.reload();
return;
}
var retry = confirm("Could not move page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\nTry again?");
if ( retry ) {
movePage();
} else {
screen1(true);
}
} );
};
var tagRedrect = function() {
$("#M2D-status0").html("Done,<br/>Tagging redirect for speedy deletion...");
API.postWithToken( 'csrf', {
action: 'edit',
title: config.mw.wgPageName,
prependtext: '{{Db-r2}}\n',
summary: '[[WP:R2|R2]] speedy deletion request (article moved to draftspace)' + config.script.advert
} )
.done( function() {
$("#M2D-task0").css({"color":"#000", "font-weight":""});
$("#M2D-status0").append(" Done!");
getImageInfo();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not tag redirect for speedy deletion:\n"+
extraJs.makeErrorMsg(c, r) + "\n\nTry again?");
if ( retry ) {
tagRedrect();
} else {
$("#M2D-task0").css({"color":"#F00", "font-weight":""});
$("#M2D-status0").append(" Skipped");
getImageInfo();
}
} );
};
//Find which images are non-free
var getImageInfo = function() {
$("#M2D-task1").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status1").html("...");
processImageInfo = function(result) {
var nonfreefiles = [];
if ( result && result.query ) {
$.each(result.query.pages, function(id, page) {
if ( id > 0 && page.categories ) {
nonfreefiles.push(page.title);
}
});
}
editWikitext(nonfreefiles);
};
API.get( {
action: 'query',
pageids: config.mw.wgArticleId,
generator: 'images',
gimlimit: 'max',
prop: 'categories',
cllimit: 'max',
clcategories: 'Category:All non-free media',
} )
.done( function(result){
$("#M2D-task1").css({"color":"#000", "font-weight":""});
$("#M2D-status1").html("Done!");
processImageInfo(result);
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not find if there are non-free files:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
getImageInfo();
} else {
$("#M2D-task1").css({"color":"#F00", "font-weight":""});
$("#M2D-status1").html("Skipped");
editWikitext([]);
}
} );
};

//Comment out non-free files, turn categories into links, add afc draft template, list any redirects
var editWikitext = function(nonfreefiles) {
$("#M2D-task2").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status2").html("...");
var redirectsList = ( !config.pagedata.redirects ) ? '' : '\n'+
'<!-- Note: The following pages were redirects to [[' + config.mw.wgPageName +
']] before draftification:\n' +
'*[[' + config.pagedata.redirects.join(']]\n*[[') + ']]\n-->\n';
var wikitext = "{{subst:AFC draft|" + config.inputdata.authorName + "}}\n" +
redirectsList +
config.pagedata.oldwikitext.replace(/\[\[\s*[Cc]ategory\s*:/g, "[[:Category:") +
"\n{{subst:Drafts moved from mainspace}}";
// non-free files
//  (derived from [[WP:XFDC]] - https://en.wikipedia.org/wiki/User:Evad37/XFDcloser.js )
if ( nonfreefiles.length > 0 ) {
// Start building regex strings
normal_regex_str = "(";
gallery_regex_str = "(";
free_regex_str = "(";
for ( var i=0; i<nonfreefiles.length; i++ ) {
// Take off namespace prefix
filename = nonfreefiles[i].replace(/^.*?:/, "");
// For regex matching: first character can be either upper or lower case, special
// characters need to be escaped, spaces can be either spaces or underscores
filename_regex_str = "[" + mw.util.escapeRegExp(filename.slice(0, 1).toUpperCase()) +
mw.util.escapeRegExp(filename.slice(0, 1).toLowerCase()) + "]" +
mw.util.escapeRegExp(filename.slice(1)).replace(/ /g, "[ _]");
// Add to regex strings
normal_regex_str += "\\[\\[\\s*(?:[Ii]mage|[Ff]ile)\\s*:\\s*" + filename_regex_str +
"\\s*\\|?.*?(?:(?:\\[\\[.*?\\]\\]).*?)*\\]\\]";
gallery_regex_str += "^\\s*(?:[Ii]mage|[Ff]ile):\\s*" + filename_regex_str + ".*?$";
free_regex_str += "\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile):\\s*)?" +
filename_regex_str;
if ( i+1 === nonfreefiles.length ) {
normal_regex_str += ")(?![^<]*?-->)";
gallery_regex_str += ")(?![^<]*?-->)";
free_regex_str += ")(?![^<]*?-->)";
} else {
normal_regex_str += "|";
gallery_regex_str += "|";
free_regex_str += "|";
}
}
// Check for normal file usage, i.e. [[File:Foobar.png|...]]
var normal_regex = new RegExp( normal_regex_str, "g");
wikitext = wikitext.replace(normal_regex, "<!-- Commented out: $1 -->");
// Check for gallery usage, i.e. instances that must start on a new line, eventually
// preceded with some space, and must include File: or Image: prefix
var gallery_regex = new RegExp( gallery_regex_str, "mg" );
wikitext = wikitext.replace(gallery_regex, "<!-- Commented out: $1 -->");
// Check for free usages, for example as template argument, might have the File: or Image:
// prefix excluded, but must be preceeded by an |
var free_regex = new RegExp( free_regex_str, "mg" );
wikitext = wikitext.replace(free_regex, "<!-- Commented out: $1 -->");
}
API.postWithToken( 'csrf', {
action: 'edit',
pageid: config.mw.wgArticleId,
text: wikitext,
summary: config.wikitext.editsummary + config.script.advert
} )
.done( function(){
$("#M2D-task2").css({"color":"#000", "font-weight":""});
$("#M2D-status2").html("Done!");
notifyAuthor();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not edit draft artice:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
editWikitext(nonfreefiles);
} else {
$("#M2D-task2").css({"color":"#F00", "font-weight":""});
$("#M2D-status2").html("Skipped");
notifyAuthor();
}
} );
};
var notifyAuthor = function() {
if ( !config.inputdata.notifyEnable ) {
updateTalk();
return;
}
$("#M2D-task3").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status3").html("...");
API.postWithToken( 'csrf', {
action: 'edit',
title: 'User talk:' + config.inputdata.authorName,
section: 'new',
sectiontitle: config.inputdata.notifyMsgHead,
text: config.inputdata.notifyMsg,
} )
.done( function(){
$("#M2D-task3").css({"color":"#000", "font-weight":""});
$("#M2D-status3").html("Done!");
updateTalk();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not edit author talk page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
notifyAuthor();
} else {
$("#M2D-task3").css({"color":"#F00", "font-weight":""});
$("#M2D-status3").html("Skipped");
updateTalk();
}
} );
};
var updateTalk = function() {
$("#M2D-task4").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status4").html("...");
//if page exists, do a regex search/repace for class/importances parameters
var processTalkWikitext = function(result) {
var talk_id = result.query.pageids[0];
if ( talk_id < 0 ) {
$("#M2D-task4").css({"color":"#000", "font-weight":""});
$("#M2D-status4").html("Done (talk page does not exist)");
draftifyLog();
return;
}
var old_talk_wikitext = result.query.pages[talk_id].revisions[0]['*'];
var new_talk_wikitext = old_talk_wikitext.replace(/(\|\s*(?:class|importance)\s*=\s*)[^\|}]*(?=[^}]*}})/g, "$1");
if ( new_talk_wikitext === old_talk_wikitext ) {
$("#M2D-task4").css({"color":"#000", "font-weight":""});
$("#M2D-status4").html("Done (no changes needed)");
draftifyLog();
return;
}
API.postWithToken( 'csrf', {
action: 'edit',
pageid: talk_id,
section: '0',
text: new_talk_wikitext,
summary: 'Remove class/importance from project banners' + config.script.advert
} )
.done( function(){
$("#M2D-task4").css({"color":"#000", "font-weight":""});
$("#M2D-status4").html("Done!");
draftifyLog();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not edit draft's talk page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
updateTalk();
} else {
$("#M2D-task4").css({"color":"#F00", "font-weight":""});
$("#M2D-status4").html("Skipped");
draftifyLog();
}
} );
};
//get talk page wikitext (section 0)
API.get( {
action: 'query',
titles: config.inputdata.newTitle.replace("Draft:", "Draft talk:"),
prop: 'revisions',
rvprop: 'content',
rvsection: '0',
indexpageids: 1
} )
.done( processTalkWikitext )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not find draft's talk page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
updateTalk();
} else {
$("#M2D-task4").css({"color":"#F00", "font-weight":""});
$("#M2D-status4").html("Skipped");
draftifyLog();
}
} );
};
var draftifyLog = function() {
if (config.doNotLog) {
$("#M2D-finished, #M2D-abort").toggle();
return;
}
$("#M2D-task5").css({"color":"#00F", "font-weight":"bold"});
$("#M2D-status5").html("...");
var logpage = 'User:' + config.mw.wgUserName + '/Draftify_log';
var monthNames = config.mw.wgMonthNames.slice(1);
var now = new Date();
var heading = '== ' + monthNames[now.getUTCMonth()] + ' ' + now.getUTCFullYear() + ' ==';
var headingPatt = RegExp(heading);
var processLogWikitext = function(result) {
var logpage_wikitext = '';
var id = result.query.pageids[0];
if ( id < 0 ) {
var createlog = confirm('Log draftification (at ' +  logpage + ') ?');
if ( !createlog ) {
$("#M2D-task5").css({"color":"#F00", "font-weight":""});
$("#M2D-status5").empty().append("Skipped");
$("#M2D-finished, #M2D-abort").toggle();
return;
}
logpage_wikitext = 'This is a log of pages moved to draftspace using the [[User:Evad37/MoveToDraft|MoveToDraft]] script.'; 
} else {
logpage_wikitext = result.query.pages[id].revisions[0]['*'].trim();
}
if ( !headingPatt.test(logpage_wikitext) ) {
logpage_wikitext += '\n\n' + heading;
}
logpage_wikitext += '\n' + config.inputdata.logMsg;
API.postWithToken( 'csrf', {
action: 'edit',
title: logpage,
text: logpage_wikitext,
summary: 'Logging [['+config.inputdata.newTitle+']]' + config.script.advert
} )
.done( function(){
$("#M2D-task5").css({"color":"#000", "font-weight":""});
$("#M2D-status5").html("Done!");
$("#M2D-finished, #M2D-abort").toggle();
} )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not edit log page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
draftifyLog();
} else {
$("#M2D-task5").css({"color":"#F00", "font-weight":""});
$("#M2D-status5").html("Skipped");
$("#M2D-finished, #M2D-abort").toggle();
}
} );
};
//get log page wikitext
API.get( {
action: 'query',
titles: logpage,
prop: 'revisions',
rvprop: 'content',
indexpageids: 1
} )
.done( processLogWikitext )
.fail( function(c,r) {
if ( r.textStatus === 'abort' ) { return; }
var retry = confirm("Could not find log page:\n"+ extraJs.makeErrorMsg(c, r)+"\n\n[Okay] to try again, or [Cancel] to skip");
if ( retry ) {
draftifyLog();
} else {
$("#M2D-task5").css({"color":"#F00", "font-weight":""});
$("#M2D-status5").html("Skipped");
$("#M2D-finished, #M2D-abort").toggle();
}
} );
};
// --- Interface screens ---
//0) Initial screen
var screen0 = function() {
$("#M2D-interface-header, #M2D-interface-content, #M2D-interface-footer").empty();
$("#M2D-interface-header").text("Move To Draft...");
$("#M2D-interface-content").text("Loading...");
grabPageData();
};
//1) User inputs
/**
 * 
 * @param {boolean} restoreValues Restore previously set values
 */
var screen1 = function(restoreValues) {
$("#M2D-interface-header, #M2D-interface-content, #M2D-interface-footer").empty();
$("#M2D-interface-header").text("Move To Draft: options");
$("#M2D-interface-content").append(
$('<div>').css('margin-bottom','0.5em').append(
$('<strong>')
.css({
display: 'block',
color: 'darkred'
}).append(
'Please ensure draftifying is appropriate per ',
extraJs.makeLink("WP:DRAFTIFY")
),
$('<label>').attr('for','M2D-option-newtitle').append(
'Move to ',
$('<b>').text('Draft:')
),
$('<input>').attr({'type':'text', 'name':'M2D-option-newtitle', 'id':'M2D-option-newtitle'})
),
$('<div>').css('margin-bottom','0.5em').append(
$('<label>').attr({'for':'M2D-option-movelog', 'id':'M2D-option-movelog-label'})
.css('display','block').text('Reason for move (log summary):'),
$('<textarea>').attr({'rows':'1', 'name':'M2D-option-movelog', 'id':'M2D-option-movelog'})
.css('width','99%')
),
$('<div>').css('margin-bottom','0.5em').append(
$('<label>').attr({'for':'M2D-option-author', 'id':'M2D-option-author-label'}).text('Author:'),
$('<input>').attr({'type':'text', 'name':'M2D-option-author', 'id':'M2D-option-author'})
),
$('<label>').attr({'for':'M2D-option-message-enable'}).append(
$('<input>').attr({'type':'checkbox', 'id':'M2D-option-message-enable'})
.prop('checked', true),
'Notify author'
),
$('<label>').attr({'for':'M2D-option-message-head', 'id':'M2D-option-message-head-label'})
.css({'display':'block', 'margin-top':'0.5em'}).text('Notification heading'),
$('<textarea>').attr({'id':'M2D-option-message-head', 'rows':'1'})
.css({'width':'99%', 'margin-bottom':'0.5em'}),
$('<label>').attr({'for':'M2D-option-message', 'id':'M2D-option-message-label'})
.css('display','block').text('Notification message:'),
$('<textarea>').attr({'id':'M2D-option-message', 'rows':'6'})
.css('width','99%'),
$('<span>').attr({'display': 'block'}).css({'font-style': 'italic', 'color':'darkred'})
.text("Please add to or replace the default notification text so the author understands your specific reasons for draftification!")
);
$('#M2D-option-movelog').val(config.wikitext.rationale);
$('#M2D-option-newtitle').val(getPageText(config.mw.wgPageName)).change(function() {
$('#M2D-option-message-head').val(
$('#M2D-option-message-head').val().trim()
.replace(/\[\[Draft\:.*?\|/, "[[Draft:" + $('#M2D-option-newtitle').val().trim() + "|")
);
$('#M2D-option-message').val(
$('#M2D-option-message').val().trim()
.replace(/\[\[Draft\:.*?\|/, "[[Draft:" + $('#M2D-option-newtitle').val().trim() + "|")
);
});
$('#M2D-option-author').val(config.pagedata.author);
$('#M2D-option-message-enable').change(function() {
$('#M2D-option-message-head').prop('disabled', !this.checked);
$('#M2D-option-message').prop('disabled', !this.checked);
});
$('#M2D-option-message-head').val(config.wikitext.notification_heading.replace(/\$1/g, getPageText(config.mw.wgPageName)));
$('#M2D-option-message').val(config.wikitext.notification.replace(/\$1/g, getPageText(config.mw.wgPageName)));
$("#M2D-interface-footer").append(
$('<button>').attr('id', 'M2D-next').text('Continue'),
$('<button>').attr('id', 'M2D-cancel').css('margin-left','3em').text('Cancel')
);
$("#M2D-cancel").click(function(){
$("#M2D-modal").remove();
});
if (restoreValues) {
$('#M2D-option-movelog').val(config.inputdata.rationale);
$('#M2D-option-newtitle').val(config.inputdata.newTitle);
$('#M2D-option-author').val(config.inputdata.authorName);
$('#M2D-option-message-enable').prop('checked', config.inputdata.notifyEnable);
$('#M2D-option-message-head').val(config.inputdata.notifyMsgHead);
$('#M2D-option-message').val(config.inputdata.notifyMsg);
}

$("#M2D-next").click(function(){
//Gather inputs
config.inputdata = {
rationale:$('#M2D-option-movelog').val().trim(),
newTitle: "Draft:" + $('#M2D-option-newtitle').val().trim(),
authorName: $('#M2D-option-author').val().trim(),
notifyEnable:$('#M2D-option-message-enable').prop('checked'),
notifyMsgHead:$('#M2D-option-message-head').val().trim(),
notifyMsg:$('#M2D-option-message').val().trim()
};
config.inputdata.logMsg = config.wikitext.logMsg
.replace(/\$1/g, getPageText(config.mw.wgPageName))
.replace(/\$2/g, config.inputdata.newTitle);
//Verify inputs
var errors=[];
if ( config.inputdata.newTitle.length === 0 ) {
errors.push("Invalid draft title");
}
if ( config.inputdata.authorName.length === 0 ) {
errors.push("Invalid user name");
}
if ( config.inputdata.rationale.length === 0 ) {
errors.push("Move log reason is empty");
}
if ( config.inputdata.notifyEnable ) {
if ( config.inputdata.notifyMsgHead.length === 0 ) {
errors.push("Notification heading is empty");
}
if ( config.inputdata.notifyMsg.length === 0 ) {
errors.push("Notification message is empty");
}
}
if ( errors.length >= 1 ) {
alert("Error:\n\n" + errors.join(";\n"));
return;
}
//start process off
screen2();
});
};
//2) Progress indicators
var screen2 = function() {
$("#M2D-interface-header, #M2D-interface-content, #M2D-interface-footer").empty();
$("#M2D-interface-header").text("Move To Draft: In progress...");
$("#M2D-interface-content").append(
$('<ul>').attr('id', 'M2D-tasks').css("color", "#888").append(
$('<li>').attr('id', 'M2D-task0').append(
'Moving page... ',
$('<span>').attr('id','M2D-status0').text('waiting')
),
$('<li>').attr('id', 'M2D-task1').append(
'Checking images... ',
$('<span>').attr('id','M2D-status1').text('waiting')
),
$('<li>').attr('id', 'M2D-task2').append(
'Editing page wikitext... ',
$('<span>').attr('id','M2D-status2').text('waiting')
),
config.inputdata.notifyEnable ?
$('<li>').attr('id', 'M2D-task3').append(
'Notifying author... ',
$('<span>').attr('id','M2D-status3').text('waiting')
)
: '',
$('<li>').attr('id', 'M2D-task4').append(
'Updating talk page banners... ',
$('<span>').attr('id','M2D-status4').text('waiting')
),
$('<li>').attr('id', 'M2D-task5').append(
'Logging... ',
config.doNotLog
? $('<span>').attr('font-size', '90%' ).text('disabled')
: $('<span>').attr('id','M2D-status5').text('waiting')
)
)
);
$("#M2D-interface-footer").append(
$('<button>').attr('id', 'M2D-abort').text('Abort uncompleted tasks'),
$('<span>').attr('id', 'M2D-finished').hide().append(
'Finished!',
$('<button>').attr('id', 'M2D-close').text('Close')
)
);
$("#M2D-close").click( function(){
$("#M2D-modal").remove();
window.location.reload();
} );
$("M2D-abort").click( function(){
API.abort();
$("#M2D-modal").remove();
window.location.reload();
} );
//Start task 0. The rest are done sequentially as each task is completed (or skipped).
movePage();
};
// --- Add link to 'More' menu (or user-specified portlet) which starts everything ---
mw.util.addPortletLink( (window.m2d_portlet||'p-cactions'), '#', 'Move to draft', 'ca-m2d', null, null, "#ca-move");
$('#ca-m2d').on('click', function(e) {
e.preventDefault();
// Add interface shell
$('body').prepend('<div id="M2D-modal">'+
'<div id="M2D-interface">'+
'<h4 id="M2D-interface-header"></h4>'+
'<hr>'+
'<div id="M2D-interface-content"></div>'+
'<hr>'+
'<div id="M2D-interface-footer"></div>'+
'</div>'+
'</div>');
// Interface styling
$("#M2D-modal").css({
"position": "fixed",
"z-index": "1",
"left": "0",
"top": "0",
"width": "100%",
"height": "100%",
"overflow": "auto",
"background-color": "rgba(0,0,0,0.4)"
});
$("#M2D-interface").css({
"background-color": "#f0f0f0",
"margin": "15% auto",
"padding": "2px 20px",
"border": "1px solid #888",
"width": "80%",
"max-width": "60em",
"font-size": "90%"
});
$("#M2D-interface-content").css("min-height", "7em");
$("#M2D-interface-footor").css("min-height", "3em");
// Initial interface content
screen0();
});

// End of function moveToDraft
};
/* ========== Log draftifications for a user ==================================================== */
function logDraftifications(username, fromDate) {
var targetUser = username;
if (!targetUser && targetUser!=="") {
var pageNameParts = config.mw.wgPageName.split('/');
targetUser = (pageNameParts.length > 1) ?  pageNameParts[1] : '';
}
$('#mw-content-text').empty();
// TODO: Form for setting user
var today = new Date().toISOString().slice(0,10);
var MoveToDraftEpoch = "2017-05-29";
$('#mw-content-text').append(
$(`<form id='draftifyLogForm' style='border: 1px solid #ccc; margin: 1em 0; padding: 0 0.5em;'>
<div style="display:inline-block;padding:0.5em">
<label for="draftifyUsername">User:</label>
<input type="text" name="username" id="draftifyUsername" />
</div>
<div style="display:inline-block;padding:0.5em">
<label for="draftifyFromDate">From date (and earlier)</label>
<input type="date" id="draftifyFromDate" name="fromDate" value="${fromDate || today}" />
</div>
<div style="display:inline-block;padding:0.5em">
<input type="submit" value="Show" />
</div>
</form>
`)
);
$('#draftifyUsername').val(targetUser);
$('#draftifyLogForm').on('submit', function(e) {
e.preventDefault();
$('#draftifyLog, #draftifyLogWikitext').show();
logDraftifications($('#draftifyUsername').val(), $('#draftifyFromDate').val());
});
$('#mw-content-text').append(
$(`<table id='draftifyLog' class='wikitable sortable' style='width:100%'>
<thead><tr>
<th scope='col'>From</th>
<th scope='col'>To</th>
<th scope='col'>Time</th>
<th scope='col'>User</th>
<th scope='col'>Reason</th>
</tr></thead>
<tbody></tbody>
<tfoot><tr>
<td colspan=5 id="draftifyStatus">Loading...</td>
</tr></tfoot>
</table>
<textarea id="draftifyLogWikitext" disabled="disabled" rows="10">
`)
);
$('#draftifyLogWikitext').val(`{|class="wikitable"
|-
!scope='col'|From
!scope='col'|To
!scope='col'|Time
!scope='col'|User
!scope='col'|Reason
|}`);
var query = {
action: "query",
format: "json",
list: "logevents",
leprop: "title|timestamp|comment|details|user",
letype: "move",
lenamespace: "0",
lelimit: "500",
lestart: (fromDate || today) + "T23:59:59Z"
};
if (targetUser) {
query.leuser = targetUser;
}
var continueInfo = {};
function onLoadMoreClick(e) {
e.preventDefault();
$('#draftifyStatus').empty().text("Loading...");
searchAndShowResults();
}
function parseLogTable(wikitext) {
API.post({
"action": "parse",
"format": "json",
"text": wikitext,
"prop": "text",
"contentmodel": "wikitext"
}).then(function(response) {
$parsedLogTable = $(response.parse.text['*']);
$('#draftifyLog tbody').empty().append(
$parsedLogTable.find('tr').slice(1)
);
});
}
function searchAndShowResults() {
API.get( $.extend({}, query, continueInfo) )
.then(function(response) {
// Store continuing info, if any
continueInfo = response.continue || {};
// Reset status, add a "Load more" if there are more results
$('#draftifyStatus').empty().append(
response.continue
? $('<a>').css("cursor", "pointer").text('Load more').click(onLoadMoreClick)
: null
);
// Filter to only MoveToDraft script moves
var draftifyEvents = response.query && response.query.logevents && response.query.logevents.filter(function(logevent) {
return logevent.params.target_ns === 118; // Moved to Draft namespace
});
var noDraftifyEvents = !draftifyEvents || !draftifyEvents.length;
switch(true) {
case noDraftifyEvents && !response.continue:
$('#draftifyStatus').empty().text(
$('#draftifyLog tbody tr').length == 0 ? "No results" : "No further results"
);
break;
case noDraftifyEvents:
// Continue with next batch of results, otherwise table will initially have no results but a load more link,
// or clicking "Load more" will appear to show "Loading..." but not actually add any results
searchAndShowResults();
break;
case !response.continue:
$('#draftifyStatus').empty().text("No further results");
/* falls through */
default:
draftifyEvents.forEach(function(logevent) {
var fromTitle = logevent.title;
var toTitle = logevent.params.target_title;
var timeOfMove = new Date(logevent.timestamp).toUTCString().replace("GMT", "(UTC)");
var user = logevent.user;
var comment = logevent.comment;
var wikitext = $('#draftifyLogWikitext').val().replace("|}", `|-
|[[${fromTitle}]]
|[[${toTitle}]]
|${timeOfMove}
|[[User:${user}|${user}]]
|${comment}
|}`);
$('#draftifyLogWikitext').val(wikitext);
parseLogTable(wikitext);
});
}
});
}
// Run by default, unless page loaded without a /username suffix
if (username || username==="") {
searchAndShowResults();
} else {
$('#draftifyLog, #draftifyLogWikitext').hide();
}
// End of function logDraftifications
}
/* ========== Setup ============================================================================= */
// Access draftifications using Special:Draftify_log/USER_NAME
var isDraftifyLogPage = config.mw.wgPageName.indexOf("Special:Draftify_log") === 0;
var isUserPage = config.mw.wgNamespaceNumber === 2 || config.mw.wgNamespaceNumber === 3;
if (isDraftifyLogPage) {
document.title = "Draftify log - Wikipedia";
$('h1').text("Draftify log");
$('#mw-content-text').empty()
.text("Loading...")
.before(
$('<span>').append(
'Note: This page only works with the ',
$('<a>').attr('href','/wiki/User:Evad37/MoveToDraft').text('MoveToDraft'),
' userscript installed.'
),
$('<hr>')
);
logDraftifications();
} else if (isUserPage) {
var user = config.mw.wgTitle.split('/')[0];
var url = mw.util.getUrl("Special:Draftify_log/" + user);
mw.util.addPortletLink( (window.m2d_portlet||'p-cactions'), url, 'Draftify log', 'ca-m2dlog', null, null, "#ca-move");
}
// Only operate in article namespace
if( config.mw.wgNamespaceNumber !== 0 ) {
return;
}
// Only operate for existing pages
if ( config.mw.wgCurRevisionId === 0 ) {
return;
}
moveToDraft();

});
// </nowiki>

/* Lourdes/PageCuration.js */
/* This script adds a "Page Curation" link to the top toolbar that points to Special:NewPagesFeed.
   It is primarily designed to assist new page reviewers. Page Curation is a feature-rich purpose-built
   system to review new pages. Be sure to have read and fully understood the instructions at New Pages Patrol.
   To use the script, add the following line to Special:MyPage/common.js:
 
importScript('User:Lourdes/PageCuration.js'); // Linkback: [[User:Lourdes/PageCuration.js]]
*/
 
$.when( mw.loader.using( ['mediawiki.util'] ), $.ready ).done( function() {
  mw.util.addPortletLink(
'p-personal',
mw.util.getUrl('Special:NewPagesFeed'),
'Page Curation',
'pt-pagecuration',
'View the new pages feed',
null,
'#pt-preferences'
  );
});

/* Novem Linguae/Scripts/CiteHighlighter.js */
//<nowiki>
class CiteHighlighter {
/** CAREFUL. This is case sensitive. */
deleteAll(haystack, ...strings) {
for ( let string of strings ) {
for ( let key in haystack ) {
haystack[key] = this.deleteFromArray(haystack[key], string);
}
}
}
deleteFromArray(haystack, needle) {
const index = haystack.indexOf(needle);
if (index > -1) {
haystack.splice(index, 1);
}
return haystack;
}
async getWikicode(title) {
if ( ! mw.config.get('wgCurRevisionId') ) return ''; // if page is deleted, return blank
var wikicode = '';
title = encodeURIComponent(title);
await $.ajax({
url: '/w/api.php?action=parse&page='+title+'&prop=wikitext&formatversion=2&format=json',
success: function (result) {
wikicode = result['parse']['wikitext'];
},
dataType: "json",
async: false
});
return wikicode;
}
async getWikitextFromCache(title) {
var api = new mw.ForeignApi('https://en.wikipedia.org/w/api.php');
var wikitext = '';
await api.get( {
action: 'query',
prop: 'revisions',
titles: title,
rvslots: '*',
rvprop: 'content',
formatversion: '2',
uselang: 'content', // needed for caching
smaxage: '86400', // cache for 1 day
maxage: '86400' // cache for 1 day
} ).done( function ( data ) {
wikitext = data.query.pages[0].revisions[0].slots.main.content;
} );
return wikitext;
}
getUnreliableWords() {
// /(blog|blogspot|caard|\/comment|fandom|forum|preprint|railfan|thread|weebly|wix|wordpress|blockchain|crypto|innovative|podcast|about|newswire|release|announce|acquire)/gm
return [
'/comment',
'about-me',
'about-us',
'/about/',
'acquire',
'announce',
//'blockchain',
'blog', // by far the most common hit
'blogspot',
'businesswire',
'caard',
'contact-us',
'contactus',
//'crypto',
'fandom',
'/forum/',
'google.com/search',
'innovative',
'newswire',
'podcast',
'/post/',
'preprint',
'press-release',
'pressrelease',
'prnews',
'railfan',
'sponsored',
'thread',
'weebly',
'wix',
'wordpress',
];
}
async getListOfSourcesAndRatings() {
let sources = await this.getWikitextFromCache('User:Novem Linguae/Scripts/CiteHighlighter/SourcesJSON.js');
sources = JSON.parse(sources);
return sources;
}
setConfigVariableDefaultsIfNeeded() {
if ( window.citeHighlighterHighlightEverything === undefined ) {
window.citeHighlighterHighlightEverything = false;
}
if ( window.citeHighlighterLighterColors === undefined ) {
window.citeHighlighterLighterColors = false;
}
if ( window.citeHighlighterAlwaysHighlightSourceLists === undefined ) {
window.citeHighlighterAlwaysHighlightSourceLists = false;
}
}
/**
  * Don't highlight certain pages, for speed and visual appearance reasons.
  *
  * On pages with a lot of links (watchlist, WP:FA), highlighting EVERYTHING will double the
  * load time. e.g. watchlist 5 seconds -> 10 seconds.
  */
isSlowPage() {
if (
mw.config.get('wgAction') == 'history' ||
this.articleTitle == 'Main_Page' ||
this.articleTitle == 'Wikipedia:Featured_articles' ||
this.articleTitle == 'Special:Watchlist'
) {
return true;
}
return false;
}
/**
  * If page is a source quality list, highlight everything, even if highlightEverything = false;
  * Goal: easily see if the script is highlighting anything wrong.
  */
highlightSourceListsMoreAggressively() {
let highlightEverythingList = [
'Wikipedia:Reliable_sources/Perennial_sources',
'Wikipedia:New_page_patrol_source_guide',
'Wikipedia:WikiProject_Albums/Sources',
'Wikipedia:WikiProject_Video_games/Sources#Reliable_sources',
'Wikipedia:WikiProject_Anime_and_manga/Online_reliable_sources',
'Wikipedia:WikiProject_Africa/Africa_Sources_List',
'Wikipedia:WikiProject_Dungeons_%26_Dragons/References',
];
if ( window.citeHighlighterAlwaysHighlightSourceLists == true) {
if ( highlightEverythingList.includes(this.articleTitle) ) {
window.citeHighlighterHighlightEverything = true;
}
}
}
/**
  * If page is a draft, highlight everything, as the # of links is small, and oftentimes
  * inline citations are malformed
  */
highlightDraftsMoreAggressively() {
if ( mw.config.get('wgNamespaceNumber') == 118 ) {
window.citeHighlighterHighlightEverything = true;
}
}
/**
  * If highlightEverything = true, delete wikipedia.org and wiktionary. Too many false positives.
  */
preventWikipediaFalsePositives() {
if ( window.citeHighlighterHighlightEverything ) {
this.deleteAll(this.sources, 'en.wikipedia.org', 'wikipedia.org', 'wiktionary.org');
this.deleteFromArray(this.unreliableWordsForOrangeHighlighting, 'wiki');
}
}
getColors() {
if ( window.citeHighlighterLighterColors ) {
return {
'unreliableWord':'#ffb347',
'preprint':'#ffcfd5',
'doi':'transparent',
'medrs': '#63ff70',
'green': '#a6ffb9',
'yellow': '#ffffcc',
'red': '#ffcfd5',
};
} else {
return {
// order of these first 3 fixes an issue where published academic papers were being colored preprint red
// lowest priority
'unreliableWord':'#ffb347', // orange for now, for easier testing. later will be red.
'preprint':'lightcoral',
'doi':'transparent',
'medrs': 'limegreen',
'green': 'lightgreen',
'yellow': 'khaki',
'red': 'lightcoral',
//'aggregator':'plum',// turning off aggregator for now, red/yellow/green is nice and simple, purple makes the color scheme more complicated
// highest priority
};
}
}
writeCSSForEachColor() {
for ( let key in this.colors ) {
mw.util.addCSS('.cite-highlighter-' + key + ' {background-color: ' + this.colors[key] + ';}');
mw.util.addCSS('.rt-tooltipTail.cite-highlighter-' + key + '::after {background: ' + this.colors[key] + ';}');
}
}
addHTMLClassesToRefs() {
for ( let color in this.colors ) {
if ( typeof this.sources[color] === 'undefined' ) continue;
for ( let source of this.sources[color] ) {
// This code makes the algorithm more efficient, by not adding CSS for sources that aren't found in the wikicode.
// I programmed some exceptions to fix bugs. For example:
// - {{Cite journal}} with a pubmed ID generates nih.gov without putting it in the wikicode
// - {{Cite tweet}} generates twitter.com without putting it in the wikicode
if ( this.wikicode.includes(source) || source === 'nih.gov' || source === 'twitter.com' ) {
// highlight external links, if it contains a period and no space (i.e. a domain name)
if ( source.includes('.') && ! source.includes(' ') ) {
// highlight whole cite
// [title="source" i]... the "i" part is not working in :has() for some reason
// use .toLowerCase() for now
// using .addClass() instead of .css() or .attr('style') because I'm having issues getting medrs to override arXiv/Wikidata/other red sources
$('li[id^="cite_note-"]').has('a[href*="/'+source.toLowerCase()+'"]').addClass('cite-highlighter-' + color);
$('li[id^="cite_note-"]').has('a[href*=".'+source.toLowerCase()+'"]').addClass('cite-highlighter-' + color);
// Also support any {{Cite}} template in a list. For example, a works cited section supporting a references section consisting of "Smith 1986, pp. 573-574" type citations. Example: https://en.wikipedia.org/wiki/C._J._Cregg#Articles_and_tweets
$('li').has('.citation a[href*="/'+source.toLowerCase()+'"]').addClass('cite-highlighter-' + color);
$('li').has('.citation a[href*=".'+source.toLowerCase()+'"]').addClass('cite-highlighter-' + color);
if ( window.citeHighlighterHighlightEverything ) {
// highlight external link only
// !important; needed for highlighting PDF external links. otherwise the HTML that generates the PDF icon has higher specificity, and makes it transparent
// [title="source" i]... the "i" means case insensitive. Default is case sensitive.
mw.util.addCSS('#bodyContent a[href*="/'+source+'" i] {background-color: '+this.colors[color]+' !important;}');
mw.util.addCSS('#bodyContent a[href*=".'+source+'" i] {background-color: '+this.colors[color]+' !important;}');
}
}
}
}
}
}
/** Observe and highlight popups created by the gadget Reference Tooltips. */
observeAndAddClassesToTooltips() {
new MutationObserver(function (mutations) {
var el = document.getElementsByClassName('rt-tooltip')[0];
if (el) {
for (let color in this.colors) {
if (typeof sources[color] === 'undefined') continue;
for (let source of sources[color]) {
if (wikicode.includes(source) || source === 'nih.gov' || source === 'twitter.com') {
if (source.includes('.') && !source.includes(' ')) {
$(el).has(`a[href*="${source.toLowerCase()}"]`).addClass('cite-highlighter-' + color);
$(el).has(`a[href*="${source.toLowerCase()}"]`).children().first().addClass('cite-highlighter-' + color);
}
}
}
}
}
}).observe(document.body, {
subtree: false,
childList: true,
});
}
/**
  * Be more aggressive with this list of words. Doesn't have to be the domain name. Can be
  * anywhere in the URL. Example unreliableWord: blog.
  */
addHTMLClassesForUnreliableWords() {
for ( let word of this.unreliableWordsForOrangeHighlighting ) {
let color = 'unreliableWord';
if ( this.wikicode.includes(word) ) {
$('li[id^="cite_note-"]').has('a[href*="'+word.toLowerCase()+'"]').addClass('cite-highlighter-' + color);
if ( window.citeHighlighterHighlightEverything ) {
mw.util.addCSS('#bodyContent a[href*="'+word+'" i] {background-color: '+this.colors[color]+' !important;}');
}
}
}
}
async execute() {
this.sources = await this.getListOfSourcesAndRatings();
this.unreliableWordsForOrangeHighlighting = this.getUnreliableWords();
this.setConfigVariableDefaultsIfNeeded();
this.articleTitle = mw.config.get('wgPageName');
if ( this.isSlowPage() ) {
return;
}
this.highlightSourceListsMoreAggressively();
this.highlightDraftsMoreAggressively();
this.preventWikipediaFalsePositives();
this.colors = this.getColors();
this.writeCSSForEachColor();
this.wikicode = await this.getWikicode(this.articleTitle);
this.addHTMLClassesToRefs();
this.addHTMLClassesForUnreliableWords();
this.observeAndAddClassesToTooltips();
}
}
// TODO: Idea from chlod: use mw.hook("wikipage.content").add( () => { rehiglight(); } ); instead. will listen for VE finishes saving or the page gets reloaded in any way. Gets called multiple times by accident sometimes though, so need to be careful not to apply duplicate classes to HTML elements.
$(async function() {
// TODO: I don't think I use mediawiki.Title. Remove that, and replace with mediawiki.Api?
await mw.loader.using(['mediawiki.util','mediawiki.Uri', 'mediawiki.Title'], async () => {
let ch = new CiteHighlighter();
await ch.execute();
});
});
//</nowiki>

/* Novem Linguae/Scripts/VoteCounter.js */
// <nowiki>
/*
- Gives an approximate count of keeps, deletes, supports, opposes, etc. in deletion discussions and RFCs.
- For AFD, MFD, and GAR, displays them at top of page.
- For everything else, displays them by the section heading.
- Counts are approximate. If people do weird things like '''Delete/Merge''', it will be counted twice.
- Adds an extra delete vote to AFDs and MFDs, as it's assumed the nominator is voting delete.
- If you run across terms that aren't counted but should be, leave a message on the talk page. Let's add as many relevant terms as we can :)
*/
$(async function() {
async function getWikicode(title) {
if ( ! mw.config.get('wgCurRevisionId') ) return ''; // if page is deleted, return blank
let api = new mw.Api();
let response = await api.get( {
"action": "parse",
"page": title,
"prop": "wikitext",
"formatversion": "2",
"format": "json"
} );
return response['parse']['wikitext'];
}
/** returns the pagename, including the namespace name, but with spaces replaced by underscores */
function getArticleName() {
return mw.config.get('wgPageName');
}
class VoteCounter {
_countRegExMatches(matches) {
return (matches || []).length;
}
_capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
_countVotes() {
// delete all strikethroughs
this.modifiedWikicode = this.modifiedWikicode.replace(/<strike>[^<]*<\/strike>/gmi, '');
this.modifiedWikicode = this.modifiedWikicode.replace(/<s>[^<]*<\/s>/gmi, '');
this.modifiedWikicode = this.modifiedWikicode.replace(/{{S\|[^}]*}}/gmi, '');
this.modifiedWikicode = this.modifiedWikicode.replace(/{{Strike\|[^}]*}}/gmi, '');
this.modifiedWikicode = this.modifiedWikicode.replace(/{{Strikeout\|[^}]*}}/gmi, '');
this.modifiedWikicode = this.modifiedWikicode.replace(/{{Strikethrough\|[^}]*}}/gmi, '');
this.votes = {};
for ( let voteToCount of this.votesToCount ) {
let regex = new RegExp("'''[^']{0,30}"+voteToCount+"(?!ing comment)[^']{0,30}'''", "gmi"); // limit to 30 chars to reduce false positives. sometimes you can have '''bold''' bunchOfRandomTextIncludingKeep '''bold''', and the in between gets detected as a keep vote
let matches = this.modifiedWikicode.match(regex);
let count = this._countRegExMatches(matches);
if ( ! count ) continue; // only log it if there's votes for it
this.votes[voteToCount] = count;
}
}
constructor(wikicode, votesToCount) {
this.originalWikicode = wikicode;
this.modifiedWikicode = wikicode;
this.votesToCount = votesToCount;
this.voteSum = 0;
this._countVotes();
if ( ! this.votes ) return;
// if yes or no votes are not present in wikitext, but are present in the votes array, they are likely false positives, delete them from the votes array
let yesNoVotesForSurePresent = this.modifiedWikicode.match(/('''yes'''|'''no''')/gi);
if ( ! yesNoVotesForSurePresent ) {
delete this.votes.yes;
delete this.votes.no;
}
for ( let count of Object.entries(this.votes) ) {
this.voteSum += count[1];
}
this.voteString = '';
for ( let key in this.votes ) {
let humanReadable = key;
humanReadable = humanReadable.replace(/\(\?<!.+\)/, ''); // remove regex lookbehind
humanReadable = this._capitalizeFirstLetter(humanReadable);
this.voteString += this.votes[key] + ' ' + humanReadable + ', ';
}
this.voteString = this.voteString.slice(0, -2); // trim extra comma at end
this.voteString = this._htmlEscape(this.voteString);
}
_convertWikicodeHeadingToHTMLSectionID(wikicode) {
// remove == == from headings
wikicode = wikicode.replace(/^=+\s*/, '');
wikicode = wikicode.replace(/\s*=+$/, '');
// remove wikilinks
wikicode = wikicode.replace(/\[\[:?/g, '');
wikicode = wikicode.replace(/\]\]/g, '');
// remove bold and italic
wikicode = wikicode.replace(/'{2,5}/g, '');
// convert spaces to _
wikicode = wikicode.replace(/ /g, '_');
return wikicode;
}
_jQueryEscape(str) {
return str.replace(/(:|\.|\[|\]|,|=|@)/g, "\\$1");
}
_doubleQuoteEscape(str) {
return str.replace(/"/g, '\\"');
}
getHeadingForJQuery() {
let firstLine = this.originalWikicode.split('\n')[0];
let htmlHeadingID = this._convertWikicodeHeadingToHTMLSectionID(firstLine);
let jQuerySearchString = '[id="' + this._doubleQuoteEscape(htmlHeadingID) + '"]';
return jQuerySearchString;
}
getVotes() {
return this.votes;
}
getVoteSum() {
return this.voteSum;
}
/* HTML escaped */
getVoteString() {
return this.voteString;
}
_htmlEscape(unsafe){
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
}
votesToCount = [
// AFD
'keep',
'delete',
'merge',
'draftify',
'userfy',
'redirect',
'stubify',
'stubbify',
'TNT',
// RFC
'support',
'oppose',
'neutral',
// move review
'endorse',
'overturn',
'relist',
'procedural close',
// GAR
'delist',
// RFC
'option 1',
'option 2',
'option 3',
'option 4',
'option 5',
'option 6',
'option 7',
'option 8',
'option A',
'option B',
'option C',
'option D',
'option E',
'option F',
'option G',
'option H',
'yes',
'no',
'bad rfc',
'remove',
'include',
'exclude',
// RSN
'agree',
'disagree',
'status quo',
'(?<!un)reliable',
'unreliable',
// RFD
'(?<!re)move',
'retarget',
'disambiguate',
'withdraw',
'setindex',
// MFD
'historical', // mark historical
// TFD
'rename',
];
// don't run when not viewing articles
let action = mw.config.get('wgAction');
if ( action != 'view' ) return;
let title = getArticleName();
// only run in talk namespaces (all of them) or Wikipedia namespace
let isEnglishWikipedia = mw.config.get('wgDBname') === 'enwiki';
if ( isEnglishWikipedia ) {
let namespace = mw.config.get('wgNamespaceNumber');
if ( ! mw.Title.isTalkNamespace(namespace) && namespace !== 4 && title != 'User:Novem_Linguae/sandbox' ) {
return;
}
}
// get wikitext
let wikicode = await getWikicode(title);
if ( ! wikicode ) return;
let isAFD = title.match(/^Wikipedia:Articles_for_deletion\//i);
let isMFD = title.match(/^Wikipedia:Miscellany_for_deletion\//i);
let isGAR = title.match(/^Wikipedia:Good_article_reassessment\//i);
if ( isAFD || isMFD || isGAR ) {
// delete everything above the first heading, to prevent the closer's vote from being counted
wikicode = wikicode.replace(/^.*?(===.*)$/s, '$1');
// add a delete vote. the nominator is assumed to be voting delete
if ( isAFD || isMFD ) {
wikicode += "'''delete'''";
}
let vc = new VoteCounter(wikicode, votesToCount);
let votes = vc.getVotes();
let voteString = vc.getVoteString();
if ( ! voteString ) return;
let percentsHTML = '';
if ( isAFD || isMFD ) {
let counts = {};
for ( let key of votesToCount ) {
let value = votes[key];
if ( typeof value === 'undefined' ) {
value = 0;
}
counts[key] = value;
}
let keep = counts['keep'] + counts['stubify'] + counts['stubbify'] + counts['TNT'];
let _delete = counts['delete'] + counts['redirect'] + counts['merge'] + counts['draftify'] + counts['userfy'];
let total = keep + _delete;
let keepPercent = keep / total;
let deletePercent = _delete / total;
keepPercent = Math.round(keepPercent * 100);
deletePercent = Math.round(deletePercent * 100);
percentsHTML = `<br /><span style="font-weight: bold;">${keepPercent}% <abbr title="Keep, Stubify, TNT">Keep-ish</abbr>, ${deletePercent}% <abbr title="Delete, Redirect, Merge, Draftify, Userfy">Delete-ish</abbr></span>`;
}
allHTML = `<div id="VoteCounter"><span style="font-weight: bold;">${voteString}</span> <small>(approximately)</small>${percentsHTML}</div>`;
$('#contentSub').before(allHTML);
} else {
// make a list of the strpos of each heading
let matches = wikicode.matchAll(/(?<=\n)(?===)/g);
let sections = [0];
for ( let match of matches ) {
sections.push(match.index);
}
let isXFD = title.match(/_for_(?:deletion|discussion)\//i);
// now that we know where the fenceposts are, calculate everything else, then inject the appropriate HTML
let sectionsLength = sections.length;
for ( let i = 0; i < sectionsLength ; i++ ) {
let startPosition = sections[i];
let lastSection = i === sectionsLength - 1;
let endPosition;
if ( lastSection ) {
endPosition = wikicode.length;
} else {
endPosition = sections[i + 1]; // Don't subtract 1. That will delete a character.
}
let sectionWikicode = wikicode.slice(startPosition, endPosition); // slice and substring (which both use (startPos, endPos)) are the same. substr(startPos, length) is deprecated.
if ( isXFD ) {
let proposeMerging = sectionWikicode.match(/'''Propose merging'''/i);
// add a vote for the nominator
if ( proposeMerging ) {
sectionWikicode += "'''merge'''";
} else {
sectionWikicode += "'''delete'''";
}
// delete "result of the discussion was X", to prevent it from being counted
sectionWikicode = sectionWikicode.replace(/The result of the discussion was(?::'')? '''[^']+'''/ig, '');
}
let vc = new VoteCounter(sectionWikicode, votesToCount);
let voteSum = vc.getVoteSum();
if ( voteSum < 3 ) continue;
let voteString = vc.getVoteString();
let allHTML = `<div id="VoteCounter" style="color: darkgreen; border: 1px solid black;"><span style="font-weight: bold;">${voteString}</span> <small>(approximately)</small></div>`;
let isLead = startPosition === 0;
if ( isLead ) {
$('#contentSub').before(allHTML);
} else {
let headingForJQuery = vc.getHeadingForJQuery(startPosition);
if ( ! $(headingForJQuery).length ) {
console.warn('User:Novem Linguae/Scripts/VoteCounter.js: ERROR: Heading ID not found. This indicates a bug in _convertWikicodeHeadingToHTMLSectionID() that Novem Linguae needs to fix. Please report this on his talk page along with the page name and heading ID. The heading ID is: ' + headingForJQuery)
}
$(headingForJQuery).parent().first().after(allHTML); // prepend is interior, before is exterior
}
}
}
// TODO: write a parser that keeps track of pairs of ''', to fix issue with '''vote''' text '''vote''' sometimes counting the text between them
// TODO: handle CFD big merge lists, e.g. https://en.wikipedia.org/wiki/Wikipedia:Categories_for_discussion/Log/2021_December_10#Category:Cornish_emigrans_and_related_subcats
});
// </nowiki>

/* Novem Linguae/Scripts/NPPLinks.js */
// <nowiki>
/*
var request = require('request').defaults({jar: true}),
url = "https://en.wikipedia.org/w/api.php";
function purge(title) {
var params = {
action: "purge",
titles: title,
format: "json"
};
request.post({ url: url, form: params }, function (error, res, body) {
if (error) {
return;
}
console.log(body);
});
}
function escapeDoubleQuotes(input) {
return input.replace(/"/g, '&quot;');
}
*/
/** pageName has namespace, undescores, no quotes, parentheses */
function buildURIComponent(wgPageName, wgNamespaceNumber, namespace, underscores, quotes, parentheses) {
let output = wgPageName;
// The order of all of these is important, because of RegEx patterns.
if ( ! namespace && wgNamespaceNumber != 0 ) output = output.replace(/^.+?:/, '');
if ( ! parentheses ) {
let matches = output.match(/^(.*)_\((.+?)\)$/);
if ( typeof matches !== 'undefined' && matches && matches[1] ) {
output = matches[1];
}
}
if ( quotes ) {
// If there's parentheses on the right, put the parentheses on the outside of the quotes, and remove the ( ) characters, but not their inner text
let matches = output.match(/^(.*)_\((.+?)\)$/);
// if parentheses on the right
if ( typeof matches !== 'undefined' && matches && matches[2] ) {
output = '"' + matches[1] + '"_' + matches[2];
} else {
output = '"' + output + '"';
}
}
if ( ! underscores ) output = output.replace(/_/g, ' ');
output = encodeURIComponent(output);
return output;
}
let sameTab = window.NPPLinksSameTab ? '' : 'target="_blank"';
// add NPP, Earwig, WP:BEFORE, CSE, Wikipedia duplicate page links to left menu
$(function() {
// only include most links for action = view and namespace = main, draft
let action = mw.config.get('wgAction');
let namespace = mw.config.get('wgNamespaceNumber');
let desiredNamespace = [0, 118].includes(namespace);
let lessLinks = false;
if ( action != 'view' || ! desiredNamespace ) {
lessLinks = true;
}
let pageName = mw.config.get('wgPageName'); // has underscores instead of spaces. has namespace prefix
let isAFD = pageName.startsWith('Wikipedia:Articles_for_deletion/');
if ( isAFD ) {
lessLinks = false;
pageName = pageName.replace('Wikipedia:Articles_for_deletion/', '');
}
// Draft:Andrew_Hill_(pharmacologist)
let underscores = buildURIComponent(pageName, namespace, true, true, false, true);
// Andrew_Hill_(pharmacologist)
let pageNameNoNamespace = buildURIComponent(pageName, namespace, false, true, false, true);
// "Andrew_Hill"_pharmacologist
let quotedName = buildURIComponent(pageName, namespace, false, true, true, true);
// "Andrew_Hill"
let quotedNoParentheses = buildURIComponent(pageName, namespace, false, true, true, false);
// "Andrew Hill" pharmacologist
let quotedNoUnderscores = buildURIComponent(pageName, namespace, false, false, true, true);
// Andrew Hill (pharmacologist)
let noUnderscores = buildURIComponent(pageName, namespace, false, false, false, true);
// "Andrew Hill"
let quotedNoUnderscoresNoParentheses = buildURIComponent(pageName, namespace, false, false, true, false);
// Andrew Hill
let noUnderscoresNoParentheses = buildURIComponent(pageName, namespace, false, false, false, false);
/*
console.log(underscores);
console.log(pageNameNoNamespace);
console.log(quotedName);
console.log(quotedNoParentheses);
console.log(quotedNoUnderscores);
console.log(noUnderscores);
console.log(quotedNoUnderscoresNoParentheses);
console.log(noUnderscoresNoParentheses);
*/
let copyvioURL = 'https://tools.wmflabs.org/copyvios/?lang=en&project=wikipedia&title='+underscores;
let webSearchURL = 'https://www.google.com/search?q='+quotedNoUnderscores+'+-wikipedia.org';
let bookSearchURL = 'https://www.google.com/search?q='+quotedNoUnderscores+'&tbm=bks';
let newsSearchURL = 'https://www.google.com/search?q='+quotedNoUnderscores+'&tbm=nws';
let newsInTitleSearchURL = 'https://www.google.com/search?q=intitle:'+quotedNoUnderscores+'&tbm=nws';
let oldNewsSearchURL = 'https://www.google.com/search?q='+quotedNoUnderscores+'%20site:news.google.com/newspapers';
let journalSearchURL = 'https://scholar.google.com/scholar?q='+quotedNoUnderscores+'';
// let profileSearchURL = 'https://scholar.google.com/citations?hl=en&view_op=search_authors&mauthors='+quotedNoUnderscoresNoParentheses+'';
let profileSearchURL = 'https://www.google.com/search?q='+noUnderscoresNoParentheses+'%20%22h-index%22';
let cseSearchURL = 'https://cse.google.com/cse?cx=007734830908295939403:galkqgoksq0&q='+quotedNoUnderscores+'';
let wikipediaDuplicateCheckURL = 'https://en.wikipedia.org/w/index.php?search='+noUnderscores+'&title=Special:Search&profile=advanced&fulltext=1&advancedSearch-current=%7B%7D&ns0=1';
let wikidataSearchURL = `https://www.wikidata.org/w/index.php?search=${quotedNoParentheses}&title=Special%3ASearch&go=Go&ns0=1&ns120=1`
let messages = '';
// WP:BEFORE wikis in other languages
if ( $('#p-lang li').length ) {
messages += "<li>WP:BEFORE check foreign wikis</li>\n";
}
// WP:BEFORE foreign script search
let articleBody = $('#mw-content-text').html();
articleBody = articleBody.replace(/(<([^>]+)>)/gi, ''); // remove HTML tags
articleBody = articleBody.trim().split('Contents')[0]; // lead only. trim everything after the word "Contents" (table of contents)
let ansiOnly = /^[\u0000-\u036f\ua792\u200b\u2009\u2061\u200e–—−▶◀•←†↓√≠≈→⋯’\u0020-\u002F\u0030-\u0039\u003A-\u0040\u0041-\u005A\u005B-\u0060\u0061-\u007A\u007B-\u007E\u00C0-\u00C3\u00C8-\u00CA\u00CC-\u00CD\u00D0\u00D2-\u00D5\u00D9-\u00DA\u00DD\u00E0-\u00E3\u00E8-\u00EA\u00EC-\u00ED\u00F2-\u00F5\u00F9-\u00FA\u00FD\u0102-\u0103\u0110-\u0111\u0128-\u0129\u0168-\u0169\u01A0-\u01B0\u1EA0-\u1EF9\u02C6-\u0323]*$/.test(articleBody); // the ones at the end are vietnamese
if ( ! ansiOnly ) {
messages += "<li>WP:BEFORE search for foreign name</li>\n";
}
/*
// debugging/logging code
console.log(articleBody);
matches = articleBody.match(/[^\u0000-\u036f\ua792\u200b\u2009\u2061\u200e–—−▶◀•←†↓√≠≈→⋯’\u0020-\u002F\u0030-\u0039\u003A-\u0040\u0041-\u005A\u005B-\u0060\u0061-\u007A\u007B-\u007E\u00C0-\u00C3\u00C8-\u00CA\u00CC-\u00CD\u00D0\u00D2-\u00D5\u00D9-\u00DA\u00DD\u00E0-\u00E3\u00E8-\u00EA\u00EC-\u00ED\u00F2-\u00F5\u00F9-\u00FA\u00FD\u0102-\u0103\u0110-\u0111\u0128-\u0129\u0168-\u0169\u01A0-\u01B0\u1EA0-\u1EF9\u02C6-\u0323]/g);
if ( matches ) {
for ( match of matches ) {
console.log(match);
console.log(match.charCodeAt(0));
}
}
*/
let links = '';
if ( ! lessLinks ) {
links += `
<li><a href="`+copyvioURL+`" `+sameTab+`>Copyvio check</a></li>
<li><a href="`+wikipediaDuplicateCheckURL+`" `+sameTab+`>Duplicate article check</a></li>
<li><a href="`+webSearchURL+`" `+sameTab+`>WP:BEFORE web</a></li>
<li><a href="`+newsSearchURL+`" `+sameTab+`>WP:BEFORE news</a></li>
<li><a href="`+oldNewsSearchURL+`" `+sameTab+`>WP:BEFORE news archive</a></li>
<li><a href="`+bookSearchURL+`" `+sameTab+`>WP:BEFORE books</a></li>
<li><a href="`+journalSearchURL+`" `+sameTab+`>WP:BEFORE scholar</a></li>
`+messages+`
<li><a href="`+profileSearchURL+`" `+sameTab+`>h-index</a></li>
<li><a href="`+cseSearchURL+`" `+sameTab+`>Google CSE</a></li>
<li><a href="`+newsInTitleSearchURL+`" `+sameTab+`>News (name in title)</a></li>
<li><a href="`+wikidataSearchURL+`" `+sameTab+`>Wikidata</a></li>
`;
/*
TODO: https://en.wikipedia.org/wiki/MediaWiki:Gadget-purgetab.js
<li><a onclick="purge(&quot;`+escapeDoubleQuotes(pageNameNoNamespace)+`&quot;);">Purge (for orphan check)</a></li>
<li><a href="">Orphan check</a></li>
*/
// TODO: display message if orphan
// TODO: display message if no categories
}
$('#p-navigation').after(`
<nav id="p-npp-links" class="mw-portlet mw-portlet-npp-links vector-menu vector-menu-portal portal" aria-labelledby="p-npp-links-label" role="npp-links">
<h3 id="p-npp-links-label" class="vector-menu-heading">
<span>New page patrol</span>
</h3>
<div class="vector-menu-content">
<ul class="vector-menu-content-list">
<li><a href="/wiki/Special:NewPagesFeed">New pages feed</a></li>
`+links+`
</ul>
</div>
</nav>
`);
});
// </nowiki>

/* Ingenuity/quickNavigate.js */
let lastPressed = -1;
window.addEventListener("keydown", event => {
if (event.key === "\\") {
if (new Date().getTime() - lastPressed < 1000) {
let input = document.createElement("input", { id: "searchInput" });
document.body.appendChild(input);
input.style.border = "none";
input.style.outline = "none";
input.style.background = "none";
input.style.position = "fixed";
input.style.top = "calc(100% - 100px)";
input.style.left = "0px";
input.style.width = "100%";
input.style.textAlign = "center";
input.style.zIndex = "5";
input.style.fontSize = "3em";
input.focus();
input.onblur = function() {
input.remove();
}
input.onkeydown = e => {
if (e.key.toLowerCase() === "enter") {
location.href = "https://en.wikipedia.org/wiki/" + input.value;
}
}
input.oninput = e => {
if (e.data === "\\") {
input.value = "";
}
}
} else {
lastPressed = new Date().getTime();
}
}
});

/* Ingenuity/cleaner.js */
const regexes = {
findReflist: /(<references \/>)|(reflist)|(===? ((Bibliography)|(Sources)|(References)) ===?)/gi,
fixRefs1: / (<ref(?: name ?= ?"[^<>]{0,30}")?>(?:(?!<\/ref>).)+?<\/ref>)/gs,
fixRefs2: / (<ref name ?= ?"[^<>]{0,30}" ?\/>)/gs,
fixRefs3: /(<ref(?: name ?= ?"[^<>]{0,30}")?>(?:(?!<\/ref>).)+?<\/ref>)[\n ]?([,\.\?\!\;])/gs,
fixRefs4: /(<ref name ?= ?"[^<>]{0,30}" ?\/>)[\n ]?([,\.\?\!\;])/gs,
headings: /^== ?(.+?) ?==$/gm,
linksInHeadings: /(?<===)(.*)\[\[(.+?)\]\](.*)(?===)/g
};
let notificationElem, infoText;
function setStyles(elem, style) {
for (let i in style) {
elem.style[i] = style[i];
}
}
function findProblems(content) {
let problems = {
reflist: { value: false, text: "Cannot find {" + "{reflist}} template" },
punctuation: { value: false, text: "Misplaced punctuation or spaces around &lt;ref&gt; tags" },
headings: { value: false, text: "Misplaced section headings" },
linksInHeadings: { value: false, text: "Links in headings" },
curlyQuotes: { value: false, text: "Curly quotes used" }
};
if (content.match(regexes.findReflist) === null) {
problems.reflist.value = true;
}
if (content.match(regexes.fixRefs1) !== null ||
content.match(regexes.fixRefs2) !== null ||
content.match(regexes.fixRefs3) !== null ||
content.match(regexes.fixRefs4) !== null) {
problems.punctuation.value = true;
}
let headings = content.match(regexes.headings);
if (headings !== null) {
headings = headings.map(x => x.replace(regexes.headings, "$1").toLowerCase());
let refsSeen = false;
for (let i of headings) {
if (i === "references") {
refsSeen = true;
}
if (refsSeen && i === "see also") {
problems.headings.value = true;
}
if (!refsSeen && i === "further reading") {
problems.headings.value = true;
}
}
if (!headings.includes("references")) {
problems.headings.value = false;
}
}
if (content.match(regexes.linksInHeadings)) {
problems.linksInHeadings.value = true;
}
if ((content.includes("“") && content.includes("”")) || content.includes("‘") || content.includes("’")) {
problems.curlyQuotes.value = true;
}
return problems;
}
function toggleShowProblems() {
if (document.querySelector("#cleanerScriptInfo") === null) {
showProblems();
} else {
document.querySelector("#cleanerScriptInfo").remove();
}
}
function runRegexes(text, list) {
for (let item of list) {
text = text.replaceAll(item[0], item[1]);
}
return text;
}
function fixPunctuation(content) {
let punctRegexes = [
[/ ?(<ref(?: name ?= ?"[^<>]{0,30}")?>(?:(?!<\/ref>).)+?<\/ref>)/gs, "$1"],
[/ ?(<ref name ?= ?"[^<>]{0,30}" ?\/>)/gs, "$1"],
[/(<ref(?: name ?= ?"[^<>]{0,30}")?>(?:(?!<\/ref>).)+?<\/ref>)[\n ]?([,\.\?\!\;])/gs, "$2$1"],
[/(<ref name ?= ?"[^<>]{0,30}" ?\/>)[\n ]?([,\.\?\!\;])/gs, "$2$1"]
];
let cleaned = runRegexes(content, punctRegexes);
while (cleaned !== content) {
content = cleaned;
cleaned = runRegexes(content, punctRegexes);
}
return cleaned;
}
function fixHeadings(content) {
let split = content.split(/(?=^\=\= ?.+? ?\=\=$)/gm);
let headingNames = split.map(n => n.match(/^== ?(.+?) ?==$/gm)).map(n => n && n[0].replace(regexes.headings, "$1").toLowerCase());
let headingsToChange = [["further reading", "references"], ["references", "see also"]];
for (let item of headingsToChange) {
let index1 = headingNames.indexOf(item[0]);
let index2 = headingNames.indexOf(item[1]);
if (index1 < index2 && index1 !== -1 && index2 !== -1) {
let elem = headingNames.splice(index2, 1);
headingNames.splice(index1, 0, elem[0]);
elem = split.splice(index2, 1);
split.splice(index1, 0, elem[0]);
}
}
for (let i = 0; i < split.length; i++) {
if (i === split.length - 1) {
continue;
}
while (!split[i].endsWith("\n\n")) {
split[i] += "\n";
}
}
return split.join("");
}
async function fixProblems() {
infoText.innerHTML = "<br>Editing...";
let elems = document.getElementsByClassName("cleanerScriptCheckbox");
elems = Array.prototype.slice.call(elems).filter(e => e.checked).map(e => e.name.split("cleaner-")[1]);
let api = new mw.Api();
let content = await getContent(api, mw.config.get("wgPageName"));
let problems = findProblems(content);
let numFixed = 0;
if (problems.punctuation.value && elems.includes("punctuation")) {
content = fixPunctuation(content);
numFixed++;
}
if (problems.reflist.value && elems.includes("reflist")) {
content += "\n==References==\n{{reflist}" + "}";
numFixed++;
}
if (problems.headings.value && elems.includes("headings")) {
content = fixHeadings(content);
numFixed++;
}
if (problems.linksInHeadings.value && elems.includes("linksInHeadings")) {
let cleaned = content;
do {
content = cleaned;
cleaned = cleaned.replace(regexes.linksInHeadings, "$1$2$3");
} while (cleaned !== content);
numFixed++;
}
if (problems.curlyQuotes.value && elems.includes("curlyQuotes")) {
content = content.replaceAll("”", "\"")
.replaceAll("“", "\"")
.replaceAll("‘", "'")
.replaceAll("’", "'");
numFixed++;
}
let token = (await api.get({
action: "query",
format: "json",
meta: "tokens"
}))["query"]["tokens"]["csrftoken"];
await api.post({
title: mw.config.get("wgPageName"),
action: "edit",
text: content,
summary: `Fixed ${numFixed} problems using [[User:Ingenuity/cleaner.js|cleaner.js]]`,
minor: true,
nocreate: true,
token: token
});
infoText.innerHTML = "<br><span style='color: green;'>Reloading...</span>";
location.reload();
}
async function getContent(api, pageTitle) {
return (await api.get({
action: "query",
prop: "revisions",
titles: pageTitle,
rvslots: "*",
rvprop: "content",
formatversion: "2"
}))["query"]["pages"][0]["revisions"][0]["slots"]["main"]["content"];
}
function inputChange() {
let elems = Array.prototype.slice.call(document.getElementsByClassName("cleanerScriptCheckbox"));
let total = elems.map(e => e.checked).reduce((p, c) => p + c);
document.getElementById("cleanerFixButton").disabled = total === 0;
}
function showProblems() {
let infoBox = document.createElement("div");
infoBox.id = "cleanerScriptInfo";
infoBox.onclick = e => e.stopPropagation();
setStyles(infoBox, {
position: "relative",
width: "400px",
height: "300px",
background: "rgb(230, 230, 230)",
left: "160px",
top: "-300px",
border: "1px solid rgb(200, 200, 200)",
textAlign: "left",
padding: "10px",
cursor: "initial"
});
for (let p in window.cleanerScript.problems) {
let problem = window.cleanerScript.problems[p];
infoBox.innerHTML += `
<input name="cleaner-` + p + `" class="cleanerScriptCheckbox" type="checkbox" ` + (!problem.value ? "disabled style='visibility:hidden'" : "checked") + ` onchange="inputChange()" />
<label style="color:` + (!problem.value ? "grey" : "black") + `; font-size: 12px;" for="cleaner-` + p + `">${problem.text}</label><br>
`;
}
let fixButton = document.createElement("button");
fixButton.innerText = "Fix selected problems";
fixButton.onclick = fixProblems;
fixButton.id = "cleanerFixButton";
fixButton.disabled = getNumOfProblems(window.cleanerScript.problems) === 0;
infoBox.appendChild(fixButton);
notificationElem.appendChild(infoBox);
let closeButton = document.createElement("span");
closeButton.innerText = "[x]";
setStyles(closeButton, {
position: "absolute",
top: "8px",
left: "calc(100% - 32px)",
color: "black",
cursor: "pointer"
});
closeButton.onclick = () => document.getElementById("cleanerScriptInfo").remove();
infoText = document.createElement("span");
setStyles(infoText, {
position: "absolute",
top: "calc(100% - 40px)",
left: "10px",
color: "#666",
width: "380px"
});
let namespace = mw.config.get("wgPageName").match(/\w+:/);
if (namespace !== null && ["User:", "User_talk:", "Wikipedia:", "Wikipedia_talk:", "Template:", "Template_talk:", "File:", "File_talk:",
 "Talk:", "Category:", "Category_talk:", "Help:"].includes(namespace[0])) {
infoText.innerHTML = "This page is in the " + namespace[0] + " namespace. Are you sure you want to run it? It may break the page."
}
infoBox.appendChild(closeButton);
infoBox.appendChild(infoText);
}
function getNumOfProblems(p) {
let total = 0;
for (let i in p) {
if (p[i].value) {
total++;
}
}
return total;
}
async function cleanMain() {
notificationElem = document.createElement("span", {
id: "cleanerScriptBanner"
});
setStyles(notificationElem, {
position: "fixed",
left: "0px",
top: "calc(100% - 50px)",
width: "160px",
textAlign: "center",
userSelect: "none",
cursor: "pointer",
fontSize: "13px"
});
notificationElem.onclick = () => {
toggleShowProblems();
}
document.body.appendChild(notificationElem);
let api = new mw.Api();
let pageTitle = mw.config.get("wgPageName");
window.cleanerScript = {};
let content = await getContent(api, pageTitle);
window.cleanerScript.problems = findProblems(content);
let total = getNumOfProblems(window.cleanerScript.problems);
notificationElem.innerText = "Found " + total + " problem" + (total === 1 ? "" : "s");
if (total > 0) {
notificationElem.style.color = "#cc6600";
}
};
try {
cleanMain();
} catch (e) {
console.log("An error occured when loading cleaner script: " + e);
}

/* Evad37/rater.js */
/***************************************************************************************************
 Rater --- by Evad37
 > Helps assess WikiProject banners.
 This script is a loader that will load the actual script from the /app.js subpage
 once Resource loader modules are loaded and the page DOM is ready.
 Source code is available at https://github.com/evad37/rater
***************************************************************************************************/
// <nowiki>
$.when(
// Resource loader modules
mw.loader.using([
"mediawiki.util", "mediawiki.api", "mediawiki.Title",
"oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows",
"oojs-ui.styles.icons-content", "oojs-ui.styles.icons-interactions",
"oojs-ui.styles.icons-moderation", "oojs-ui.styles.icons-editing-core",
"mediawiki.widgets", "mediawiki.widgets.NamespacesMultiselectWidget",
]),
// Page ready
$.ready
).then(function() {
var conf = mw.config.get(["wgNamespaceNumber", "wgPageName"]);
// Do not operate on Special: pages, nor on non-existent pages or their talk pages
if ( conf.wgNamespaceNumber < 0 || $("li.new[id|=ca-nstab]").length ) {
return;
}
// Do not operate on top-level User and User_talk pages (only on subpages)
if (
conf.wgNamespaceNumber >= 2 &&
conf.wgNamespaceNumber <= 3 &&
conf.wgPageName.indexOf("/") === -1
) {
return;
}
// Otherwise, load the rest of the script.
// Get the title using template substitution (so the same source file be used on both main and sandbox scripts)
var title = /* </nowiki> */ "User:Evad37/rater/app.js"; /* <nowiki> */
mw.loader.load( "https://en.wikipedia.org/w/index.php?title="+title+"&action=raw&ctype=text/javascript" );
});
// </nowiki>

/* Danski454/stubsearch.js */
// <nowiki>
//from a request by Insertcleverphrasehere
//based on [[User:Ais523/stubtagtab2.js]], which is in turn based on [[User:MC10/stubtagtab.js]]
//variables
var SSunfilteredArray = ["stub"];//does not contain parent
var stubTags = {"stub":{display:"{{Stub}}", parents:["stubs"], children:null, descendants:null, template:"Stub"},"stubs":{display:"Stubs", parents:null, children:[], descendants:["stub"], template:null}};//generic stub is not loaded by the normal method
var SSfilteredArray = ["stub"];
var stublists = ['Architecture', 'Commerce', 'Culture', 'Education', 'Geography', 'Government, law, and politics',
'History', 'Leisure', 'Military and weaponry', 'Organizations', 'People',
'Religion, mythology, faiths, and beliefs', 'Science', 'Sports', 'Technology', 'Transport',
'Miscellaneous'];
var stubTemplates = ["{{stub}}"];
var SSactive = false;
var SScurrent = stubTags["stubs"];
var SSoldid = null;
var SSnewid = null;
var SSpagesLoaded = 0;
var SSextraChildren = [{parent:"education stubs", children:["school stubs", "university stubs"]}, 
{parent:"company stubs", children:["Aeronautical company stubs", "Airline stubs", "Animation studio stubs", "Brand name products stubs", "Chemical company stubs", 
"Disney stubs", "Energy company stubs", "Fashion company stubs", "Film company stubs", "Financial services company stubs", "Food company stubs", "Hotel stubs", 
"Industrial company stubs", "Information technology company stubs", "Leisure company stubs", "Manufacturing company stubs", "Media company stubs", 
"Motor vehicle company stubs", "Music industry company stubs", "Musical instrument company stubs", "Petroleum company stubs", "Publishing company stubs", 
"Restaurant stubs", "Retail company stubs", "Service company stub", "Technological company stubs", "Telecommunications company stubs", "Toy company stubs", 
"Transport company stubs", "Video game company stubs", "African company stubs", "Asian company stubs", "European company stubs", "Canadian company stubs", 
"Mexican company stubs", "United States company stubs", "Australian company stubs", "South American company stubs"]}, 
{parent:"politics stubs", children:["Asia politics stubs", "Europe politics stubs", "Canada politics stubs", "United States politics stubs", "Mexican politics stubs", 
"New Zealand politics stubs", "South America politics stubs", "Political party stubs", "Political people stubs", "Politician stubs"]}, 
{parent:"politician stubs", children:["African politician stubs", "Asian politician stubs", "European politician stubs", "North American politician stubs", 
"Oceanian politician stubs", "South American politician stubs"]}, 
{parent:"history stubs", children:["American Old West stubs", "Ancient Near East stubs", "Ancient Egypt stubs", "Ancient Greece stubs", "Ancient Rome stubs", 
"Byzantine Empire stubs", "Kingdom of France stubs", "Mesoamerica stubs", "Nazi Germany stubs", "Netherlands Antilles stubs", "Ottoman Empire stubs", 
"Pre-Columbian era stubs", "Soviet Union stubs", "Yugoslavia stubs", "African diaspora stubs", "East Slavic history stubs", "Jewish history stubs", 
"African history stubs", "Asian history stubs", "European history stubs", "Canadian history stubs", "Mexican history stubs", "United States history stubs", 
"South American history stubs", "Military history stubs"]}, 
{parent:"people stubs", children:["Academic biography stubs", "Artist stubs", "Astronaut stubs", "Business biography stubs", "Entertainer stubs", "Explorer stubs", 
"Fashion biography stubs", "Graphic designer stubs", "Journalist stubs", "Medical biography stubs", "Model stubs", "Paramilitary biography stubs", "Piracy stubs", 
"Religious biography stubs", "Scientist stubs", "Writer stubs", "African people stubs", "Asian people stubs", "European people stubs", "North American people stubs",
"Caribbean people stubs", "Central American people stubs", "Oceanian people stubs", "South American people stubs", "Nobility stubs", "Royalty stubs"]}, 
{parent:"scientist stubs", children:["Asian scientist stubs", "African scientist stubs", "Australian scientist stubs", "European scientist stubs", "North American scientist stubs", 
"South American scientist stubs"]}, 
{parent:"science stubs", children:["scientist stubs", "Biology stubs", "Botany stubs", "Animal stubs", "Astronomy stubs", "Chemistry stubs", "Atmospheric science stubs", 
"Geology stubs", "Oceanography stubs", "Physics stubs", "Health stubs", "Anthropology stubs", "Library and information science stubs", "Sociology stubs", "Natural science stubs"]}, 
{parent:"chordate stubs", children:["Amphibian stubs", "Bird stubs", "Fish stubs", "Mammal stubs", "Reptile stubs"]}, //Amphibians, Birds, Fish, Mammals, and Reptiles
{parent:"protostome stubs", children:["Mollusc stubs"]}, 
{parent:"arthropod stubs", children:["insect stubs", "Arachnid stubs"]}, 
{parent:"technology stubs", children:["Computing stubs", "Software stubs", "Telecommunications stubs", "Automotive part stubs", "Automotive technology stubs", "Electronics stubs", 
"Energy stubs", "Engineering stubs", "Industry stubs", "Metalworking stubs", "Robotics stubs", "Sound technology stubs", "Tool stubs", "Woodworking stubs"]}, 
{parent:"transport stubs", children:["Africa transport stubs", "Asia transport stubs", "Europe transport stubs", "North America transportation stubs", "Oceania transport stubs", 
"South America transport stubs", "Space stubs", "Vehicle stubs", "Water transport stubs", "Aviation stubs", "Rail transport stubs", "Road stubs"]}, 
{parent:"military stubs", children:["African military stubs", "Argentine military stubs", "Asian military stubs", "Australian military stubs", "Canadian military stubs", 
"European military stubs", "Mexican military stubs", "United States military stubs", "Military personnel stubs"]}, 
{parent:"organization stubs", children:["Trade union stubs", "Art organization stubs", "Automotive organization stubs", "Business organization stubs", 
"Criminal organization stubs", "Education organization stubs", "Environmental organization stubs", "Film organization stubs", "Fraternity and sorority stubs",
"Freemasonry stubs", "International organization stubs", "Lesbian, gay, bisexual, transgender (LGBT) organization stubs", "Linguistics organization stubs",
"Medical organization stubs", "Music organization stubs", "Non-profit organization stubs", "Paramilitary organization stubs", "Political organisation stubs", 
"Professional association stubs", "Religious organization stubs", "Scientific organization stubs", "Youth organization stubs", "African organization stubs", 
"Asian organization stubs", "Australian organisation stubs", "Canadian organization stubs", "European organisation stubs", "Mexican organization stubs", 
"New Zealand organisation stubs", "South American organization stubs", "United States organization stubs"]}];
//functions
function SSloadDiff(){
window.location.href = mw.config.get('wgScript') + '?diff=' + SSnewid + '&oldid=' + SSoldid;
}
function SSreduceArray(unreducedArray){
var seen = {};
return unreducedArray.filter(function(item) {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
}
function SSescape(text, html){
if (html){
return text
.replace('"', "&quot;")
.replace("&", "&amp;")
.replace("'", "&#039;")
.replace("<", "&lt;")
.replace(">", "&gt;");
} else {
return text
.replace('"', "\\x22")
.replace("&", "\\x26")
.replace("'", "\\x27")
.replace("<", "\\x3c")
.replace(">", "\\x3e");
}
}
function SSgenerateDescendants(start){
if (stubTags[start].descendants !== null && stubTags[start].descendants.length !== 0){//already generated descendants
return stubTags[start].descendants.concat([start]);
}
if (stubTags[start].children === null || stubTags[start].children.length === 0){//no descendants
return [start];//for the rest, we are the only descendant
}
stubTags[start].children.forEach( function (value, index, array) {
stubTags[start].descendants = stubTags[start].descendants.concat(SSgenerateDescendants(value));//add children's descendants to our own
});
stubTags[start].descendants = SSreduceArray(stubTags[start].descendants);//remove duplicates
return stubTags[start].descendants.concat([start]);
}
function SSaddchild(parent, child){
if (stubTags.hasOwnProperty(parent)){//check parent exists
if (stubTags[parent].template !== null){//check if parent is a template, if so call this on its parents
stubTags[parent].parents.forEach(function (value, index, array) { SSaddchild(value, child); });
return;
}
//check if parent already has child
if (stubTags[parent].children.indexOf(child) !== -1){
return;
}
stubTags[parent].children.push(child);//add child to parents children
} else {//create a parent
stubTags[parent] = {display:parent, parents:[], children:[child], descendants:[], template:null};
stubTags.stubs.descendants.push(parent);//add to stubs' descendants
}
if (stubTags.hasOwnProperty(child)){//check if child exists
stubTags[child].parents.push(parent);//add parent to child
} else {//create child
stubTags[child] = {display:child, parents:[parent], children:[], descendants:[], template:null};
stubTags.stubs.descendants.push(child);//add to stubs' descendants
}
}
function SSconvertToTemplate(parent, template){
if (stubTags[parent].children === null){//already a template
return;
}
stubTags[parent].template = template;//set up template and display
stubTags[parent].display = '{{'+ template +'}}';
stubTemplates.push(('{{'+ template +'}}'));
stubTags[parent].children.forEach( function (value, index, array) {//remove all children's parent reference
index = stubTags[value].parents.indexOf(parent);//don't need index
if (index > -1) {
stubTags[value].parents.splice(index, 1);
}
SSaddchild(parent, value);//try to readd this as a child of parent
});
stubTags[parent].children = null;//clear the array
}
function SSdoExtraChildren(){
$.each(SSextraChildren, function (){ //do children in array
var parent = this.parent;
this.children.forEach(function (value, index, array){
SSaddchild(parent, value.toLowerCase());
});
});
addStubType("Unlisted stubs", "cat", "stubs");//special case for unlisted stubs
}
function stubFilter(){
filterString = document.getElementById("stub-search-search").value;
//Special case: no filterString then only show direct children
if (filterString.length === 0){
SSfilteredArray = SScurrent.children;
SSrerenderList();
return;
}
var filteringArray = SSunfilteredArray; //copy unfiltered array
filterString = filterString.toLowerCase(); //force lowercase
var filterArray = filterString.split(" "); //split string
for (var filterIndex = 0; filterIndex < filterArray.length; filterIndex++) {
SSfilteredArray = [];//clear the filtered output
filterString = filterArray[filterIndex];
for (var i = 0; i < filteringArray.length; i++) {
var testString = filteringArray[i];
if (testString.indexOf(filterString) > -1){
SSfilteredArray.push(testString);
}
}
filteringArray = SSfilteredArray;
}
SSrerenderList();
}
function loadStubs(){
for (var i = 0; i < stublists.length; i++) {
var stubType = stublists[i];
$.ajax({
url: mw.config.get("wgServer") + mw.config.get("wgScriptPath") + '/api.php?action=parse&prop=text&text=' + encodeURIComponent('__NOTOC____NOEDITSECTION__\{\{Wikipedia:WikiProject Stub sorting/Stub types/' + stubType + '}}') + '&format=json',
dataType: "json",
success: stubSearchLoadSome,
error: function() { 
SSpagesLoaded++;//increment loaded pages on failure so the menu auto loads
console.log("Stub search failed to load a stub list.");
}
});
}
//special case for unlisted stubs
$.ajax({
url: mw.config.get("wgServer") + mw.config.get("wgScriptPath") + '/api.php?action=parse&prop=text&text=' + encodeURIComponent('__NOTOC____NOEDITSECTION__\{\{User:Danski454/unlisted stubs}}') + '&format=json',
dataType: "json",
success: stubSearchLoadSome,
error: function() { 
SSpagesLoaded++;//increment loaded pages on failure so the menu auto loads
console.log("Stub search failed to load a stub list.");
}
});
SSdoExtraChildren();
}
function stubSearchLoadSome(data) {
var parseData = $(data.parse.text["*"]);
var parent = "stubs";
if (parseData.find("h1, h2").length){
switch (parseData.find("h1, h2").first().text().toLowerCase()){
case "sports":
parent = "sports stubs";
break;
case "culture":
parent = "culture stubs";
break;
case "contents":
parent = "architecture stubs";
break;
}
} else if (parseData.find("h3").length === 0) {//special case for unlisted stubs
parent = "unlisted stubs";
}
parseData.children("ul").children('li').each( function () {
var val = $(this);//'this' must be converted to jQuery
while (val.find("small").length !== 0) {//remove small tags (copied from toggleSmall.js)
var tag = val.find("small").first();
tag.before(tag.html());
tag.remove();
}
$('dl').remove();//remove description lists
generateStubData(val, parent);
});
if (parent !== "stubs" && parent !== "unlisted stubs"){
stubTags[parent].children.shift();//remove from its own children
stubTags[parent].parents = [];//remove from its own parents
SSaddchild("stubs", parent);//make a child of stubs
}
SSpagesLoaded++;
if (SSpagesLoaded == stublists.length + 1){//one list is not listed in the array
stubFilter();
}
}
function addStubType(obj, type, parent){
var name = obj.toLowerCase();
SSaddchild(parent, name);//this creates the object
if (type === "template"){
SSconvertToTemplate(name, obj);
} else {
stubTags[name].display = obj;//set display name
}
SSunfilteredArray.push(name);
SSfilteredArray.push(name);
}
function generateStubData(data, parent){
var cat;
cat = data.children("b").first().text();//categories are in bold
if (cat !== ""){
addStubType(cat, "cat", parent);
parent = cat.toLowerCase();
}
data.children("ul").children('li').each( function () {
generateStubData($(this), parent);
});
data.children("a").each( function () {//template links are not wrapped
var tag = $(this);
if (tag[0].previousSibling !== null && tag[0].previousSibling.nodeValue.endsWith("as a child of {{")){//get text before element and make sure it does not say x is a child of y
return true;
}
if (tag[0].previousSibling !== null && tag[0].previousSibling.nodeValue.endsWith("child of {{")){//get text before element and handle next element is child of x
var next = tag.next();//get next list item
if (next.is("ul")){//if next is a list get the first item of it
next = next.children("li").first();
}
SSaddchild(tag.text().toLowerCase(), next.children('b').first().text().toLowerCase());//make next link a child of us
return true;
}
if (tag.attr("href").search(/(wiki\/|=)Template:/) === -1){//must link to a template
return true;
}
addStubType(tag.text(), "template", parent);
});
}
function SSrerenderList(){
$('#stub-search-scroll').empty();
SSfilteredArray = SSreduceArray(SSfilteredArray); //remove duplicates
SSfilteredArray.forEach(function (value, index, array) {
var tag = stubTags[value];
if (index > 0){
$('#stub-search-scroll').append("•");
}
$('#stub-search-scroll').append('<input type="button" value="'+ tag.display +'" onclick="selectStub(\''+ SSescape(value, false) +'\');" style="background:none!important;color:inherit;border:none;cursor:pointer;" class="stub-search-type" />');
});
}
function SSrenderTemplate(){
$('#stub-search-scroll').empty();
$('#stub-search-scroll').append('<span id="stub-preview">Preview loading</span>');
$.ajax({
url: mw.config.get("wgServer") + mw.config.get("wgScriptPath") + '/api.php?action=parse&prop=text&text=' + encodeURIComponent('__NOTOC____NOEDITSECTION__\{\{' + SScurrent.template + '}}') + '&format=json',
dataType: "json",
success: function (data){
$('#stub-preview').before('<input type="button" value="Add {{'+ SSescape(SScurrent.template, true) +'}} to this article" onclick="addStub();" id="stub-search-confirm" />  <input type="button" value="Replace old stub templates with {{'+ SSescape(SScurrent.template, true) +'}}" onclick="replaceStub(false);" id="stub-search-confirm-replace" />  <input type="checkbox" value="load diff" id="stub-search-load-diff" checked /> Load changes<br />');
$('#stub-preview').html($(data.parse.text["*"]));
}
});
}
function stubSearchUp(){
var stub = $('#stub-search-parent-select option:selected').val();
selectStub(stub);
}
function loadStubSearch(){
if (SSactive){
return;
}
SSactive = true;
switch ( mw.config.get('skin') ){
case 'cologneblue':
$('#mw-content-text').before('<div id="stub-search-box" style="background-color:LightBlue;height:200px;border:1px solid black;padding:15px;">Search: <input type="text" id="stub-search-search"><input id="stub-search-button" type="button" value="Search" onclick="stubFilter();" /> Current selection: <span id="stub-search-cat">Stubs</span> <input id="stub-search-up" type="button" value="Go up to" onclick="stubSearchUp();" /><select id="stub-search-parent-select"></select> <input id="stub-search-diff-button" type="button" value="Load changes" onclick="SSloadDiff();" /><br /><div style="max-height:175px;overflow:auto;" id="stub-search-scroll">Loading stub templates: Please wait</div></div>');
break;
case 'timeless':
$('#bodyContent').before('<div id="stub-search-box" style="background-color:LightBlue;height:200px;border:1px solid black;padding:15px;">Search: <input type="text" id="stub-search-search"><input id="stub-search-button" type="button" value="Search" onclick="stubFilter();" /> Current selection: <span id="stub-search-cat">Stubs</span> <input id="stub-search-up" type="button" value="Go up to" onclick="stubSearchUp();" /><select id="stub-search-parent-select"></select> <input id="stub-search-diff-button" type="button" value="Load changes" onclick="SSloadDiff();" /><br /><div style="max-height:150px;overflow:auto;" id="stub-search-scroll">Loading stub templates: Please wait</div></div>');
break;
default:
$('#bodyContent').before('<div id="stub-search-box" style="background-color:LightBlue;height:200px;border:1px solid black;padding:15px;">Search: <input type="text" id="stub-search-search"><input id="stub-search-button" type="button" value="Search" onclick="stubFilter();" /> Current selection: <span id="stub-search-cat">Stubs</span> <input id="stub-search-up" type="button" value="Go up to" onclick="stubSearchUp();" /><select id="stub-search-parent-select"></select> <input id="stub-search-diff-button" type="button" value="Load changes" onclick="SSloadDiff();" /><br /><div style="max-height:175px;overflow:auto;" id="stub-search-scroll">Loading stub templates: Please wait</div></div>');
}
$('#stub-search-up, #stub-search-parent-select, #stub-search-diff-button').hide();//up button, drop down and load diff button default to hidden
loadStubs();
}
function selectStub(name){
SSgenerateDescendants(name);//generate the descendants, if they have not already been.
SScurrent = stubTags[name];
$('#stub-search-cat').text(SScurrent.display);
document.getElementById("stub-search-search").value = "";//clear search
$('#stub-search-parent-select').empty();//clear old parents selector
if (SScurrent.parents !== null){
$('#stub-search-up, #stub-search-parent-select').show();//show parent selector and button
if (SScurrent.parents[0] === "unlisted stubs") {//special case
$('#stub-search-parent-select').append('<option value="'+ SSescape("stubs", true) +'">'+ SSescape(stubTags["stubs"].display, true) +'</option>')
}
SScurrent.parents.forEach(function (value, index, array){
$('#stub-search-parent-select').append('<option value="'+ SSescape(value, true) +'">'+ SSescape(stubTags[value].display, true) +'</option>')
});
} else {
$('#stub-search-up, #stub-search-parent-select').hide();//hide parent selector and button
}
if (SScurrent.template === null){
SSunfilteredArray = SScurrent.descendants;
stubFilter();
} else {
SSrenderTemplate();
}
}
function addStub(){
replaceStub(true);//functions merged
}
function replaceStub(keepOld){//adds a stub template, optionally keeping old ones, this is copied from [[WP:US/G]]
$.getJSON(
mw.util.wikiScript('api'),
{
format: 'json',
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
titles: mw.config.get( 'wgPageName' )
}
)
.done(function ( data ) {
var page, wikitext;
try {
for ( page in data.query.pages ) {
wikitext = data.query.pages[page].revisions[0]['*'];
replaceStubWikitext( wikitext, keepOld );
}
} catch ( e ) {
console.log(1);
alert( 'Edit failed' );
}
})
.fail( function() {
console.log(2);
alert( 'Edit failed' );
});
}
function replaceStubWikitext(data, keepOld){
added_text = '\n\n\n{{'+ SScurrent.template +'}}';
stubTemplates.forEach(function (value, index, array){
var re = new RegExp(value, "gi");
if (keepOld && data.match(re) !== null){
added_text = added_text + '\n' + value;//readd stub template to bottom
}
data = data.replace(re, "");//replace stubs with nothing using unescaped regex
});
data = data.replace(/\s+$/g, "");//remove trailing whitepace, do not use m flag
data = data + added_text;
console.log(data);
var summary = 'Replaced stub templates with {{'+ SScurrent.template +'}} using [[User:Danski454/stubsearch|a tool]]';
if (keepOld){
summary = 'Added {{'+ SScurrent.template +'}} using [[User:Danski454/stubsearch|a tool]]';
}
//save data
$.ajax({
url: mw.util.wikiScript( 'api' ),
type: 'POST',
dataType: 'json',
data: {
format: 'json',
action: 'edit',
title: mw.config.get( 'wgPageName' ),
text: data, 
summary: summary,
token: mw.user.tokens.get( 'csrfToken' )
}
})
.done (function( data ) {
if ( data && data.edit && data.edit.result && data.edit.result == 'Success') {
if (SSoldid === null){
if (data.edit.oldrevid !== undefined){
SSoldid = data.edit.oldrevid;
} else {
alert("Unable to determine oldid");
}
}
if ($('#stub-search-load-diff').is(':checked') && data.edit.newrevid !== undefined){
if (SSoldid !== null && data.edit.oldrevid !== undefined){
window.location.href = mw.config.get('wgScript') + '?diff=' + data.edit.newrevid + '&oldid=' + SSoldid;//load diff
} else {
alert("Unable to load diff");
}
} else {
if (keepOld){
alert( 'Successfully added {{'+ SScurrent.template +'}} to the article' );
} else {
alert( 'Successfully replaced stubs with {{'+ SScurrent.template +'}}' );
}
if (data.edit.newrevid !== undefined){
SSnewid = data.edit.newrevid;
} else {
alert("Unable to determine newid");
}
if (SSoldid !== null && SSnewid !== null && $('#stub-search-diff-button').is(':hidden')){
$('#stub-search-diff-button').show();//add a load diff button if we have not loaded the diff
}
selectStub('stubs');//reset the UI, user probably wans to add another template
}
} else {
console.log(3);
alert( 'Edit failed' );
}
})
.fail ( function() {
console.log(4);
alert( 'Edit failed' );
});
}
$(document).ready(function(){
if ( mw.config.get( 'wgNamespaceNumber' ) === 0 || mw.config.get( 'wgPageName' ) === "Wikipedia:Sandbox" ) {//usable on articles and the sandbox (for testing and practice)
var portletLink = mw.util.addPortletLink('p-cactions', '#', 'Stub search', 'ca-stub-search', 'Tag this article as a stub', '');
$(portletLink).click(loadStubSearch);
}
});
// </nowiki>

/* Ingenuity/preview.js */
const mwapi = new mw.Api();
let previewElem = null;
async function getPageHTML(page) {
try {
return (await mwapi.get({
action: "parse",
page: page,
prop: "text",
formatversion: 2
})).parse.text;
} catch (err) {
return "<i>Page has no content, or an error occured when attempting to fetch the page</i>";
}
}
function addCloseButton() {
previewElem.innerHTML += `<div style="position:absolute;left:calc(100% - 50px);top:0;padding:10px;cursor:pointer;user-select:none;" onclick="closePreview()">[x]</div>`;
}
function closePreview() {
previewElem.remove();
previewElem = null;
const content = document.getElementById("content");
content.style.width = "initial";
content.style.height = "initial";
content.style.overflowY = "initial";
}
async function showPagePreview(page) {
if (previewElem === null) {
const content = document.getElementById("content");
content.style.width = "calc(50% - 145px)";
content.style.height = "calc(100% - 80px)";
content.style.overflowY = "auto";
previewElem = document.createElement("div");
previewElem.style.width = "calc(50% - 84px)";
previewElem.style.overflowX = "auto";
previewElem.style.position = "absolute";
previewElem.style.top = "80px";
previewElem.style.left = "calc(50% + 84px)";
previewElem.style.padding = "20px";
previewElem.style.paddingTop = "0";
previewElem.style.boxSizing = "border-box";
previewElem.style.background = "white";
previewElem.style.borderLeft = "1px solid #ccc";
document.body.appendChild(previewElem);
previewElem.innerHTML = "<div class='vector-body'><h1 style='margin-top:0'>" + page.replaceAll("_", " ") + "<a style='font-size:0.7em;margin-left:20px;' href='/w/index.php?title=" + page + "&action=history'>history</a></h1>" + "Loading page content..." + "</div>";
previewElem.innerHTML = "<div class='vector-body'><h1 style='margin-top:0'>" + page.replaceAll("_", " ") + "<a style='font-size:0.7em;margin-left:20px;' href='/w/index.php?title=" + page + "&action=history'>history</a></h1>" + (await getPageHTML(page)) + "</div>";
previewElem.style.height = Math.max(Math.min(content.clientHeight, window.innerHeight - 80), 400) + "px";
previewElem.style.overflowY = "auto";
addCloseButton();
for (let elem of Array.prototype.slice.call(previewElem.querySelectorAll(".mw-editsection"))) {
elem.remove();
}
for (let elem of Array.prototype.slice.call(previewElem.querySelectorAll(".toc"))) {
elem.remove();
}
} else {
previewElem.innerHTML = "<div class='vector-body'><h1 style='margin-top:0'>" + page.replaceAll("_", " ") + "<a style='font-size:0.7em;margin-left:20px;' href='/w/index.php?title=" + page + "&action=history'>history</a></h1>" + (await getPageHTML(page)) + "</div>";
addCloseButton();
}
}
window.addEventListener("click", (event) => {
if (event.target.tagName.toLowerCase() === "a" && event.shiftKey) {
event.preventDefault();
showPagePreview(event.target.href.split("/wiki/")[1]);
}
});

/* Enterprisey/cv-revdel.js */
// <nowiki>
$( function () {
var ADVERT = " ([[User:Enterprisey/cv-revdel|cv-revdel]])";
var SUMMARY = "Requesting copyvio revdel" + ADVERT;
var SUMMARY_TALK = "Add note about recent copyvio" + ADVERT;
var CCLEAN_TPL = "Cclean";
var pageName; // Current page name
var urlCounter = 1;
var api;
/**
 * Make a link to a given oldid of the current page.
 */
function makeOldidLink( oldid ) {
var href = "/w/index.php?title=" + encodeURIComponent( pageName ) +
"&oldid=" + oldid;
var link = document.createElement( "a" );
link.href = href;
link.textContent = oldid;
return link;
}
/**
 * Add the appropriate copyvio-revdel template to the current page.
 */
function addCvRevdel( urls ) {
var deferred = $.Deferred();
var template = "{" + "{copyvio-revdel";
template += urls.map( function ( u, idx ) {
var num = idx == 0 ? "" : ( idx + 1 );
return "|url" + num + "=" + u;
} ).join( "" );
var rows = document.querySelectorAll( "#cv-revdel tr" );
var num;
for( var i = 1, n = rows.length; i < n; i++ ) {
template += "|start" + i + "=" + rows[i].childNodes[0].textContent
+ ( rows[i].childNodes[2].childNodes[0].checked ? ( "|end" + i + "=" + rows[i].childNodes[1].textContent ) : "" );
}
template += "}}";
api.postWithToken( "csrf", {
action: "edit",
title: pageName,
summary: SUMMARY,
prependtext: template + "\n"
} ).done( function ( d ) {
if( d && d.edit && d.edit.result && d.edit.result == "Success" ) {
$( "#cv-rd-status" ).text( "Success!" );
deferred.resolve();
} else if( d && d.error ) {
$( "#cv-rd-status" ).html( "Error! Edit failed: " + d.error.info );
console.log( d );
deferred.reject();
} else {
$( "#cv-rd-status" ).html( "Error! Edit failed." );
console.log( d );
deferred.reject();
}
} ).fail( function ( code, result ) {
console.log( code, result );
if( result && result.error && result.error.spamblacklist ) {
$( "#cv-rd-status" ).html( "Error! The following URLs were on the <a href='" + mw.util.getUrl( "MediaWiki:Spam-blacklist" ) + "'>spam blacklist</a>: " + result.error.spamblacklist.matches.join( ", " ) + ". Consider removing the 'https://' or 'http://' from the beginning to make them not links." );
} else {
$( "#cv-rd-status" ).html( api.getErrorMessage( result ) );
}
deferred.reject();
} );
return deferred;
}
function addCclean( urls ) {
var deferred = $.Deferred(),
template = "{" + "{subst:cclean|url=" + urls.join( " " ) + "}}",
talkNs = mw.config.get( "wgNamespaceNumber" ) | 1,
talkNsName = mw.config.get( "wgFormattedNamespaces" )[ talkNs ],
talkPage = talkNsName + ":" + mw.config.get( "wgTitle" );
api.postWithToken( "csrf", {
action: "edit",
title: talkPage,
summary: SUMMARY_TALK,
appendtext: "\n\n" + template
} ).done( function ( d ) {
if( d && d.edit && d.edit.result && d.edit.result == "Success" ) {
deferred.resolve();
} else {
deferred.reject();
console.log( d );
}
} ).fail( function ( code, result ) {
deferred.reject();
$( "#cv-rd-status" ).append( "Also, while editing " + talkPage +
", this error happened: " + api.getErrorMessage( result ) );
console.log( code, result );
} );
return deferred;
}
/**
 * Load the main cv-revdel panel and add buttons/other stuff to
 * the history UI
 */
function load( evt ) {
if( evt ) evt.preventDefault();
// Don't load the panel for a second time if it's already there
if( document.getElementById( "cv-revdel" ) ) return;
api = new mw.Api();
// Style for the panel
mw.util.addCSS(
"#cv-revdel { border: thin solid rgb(197, 197, 197); " +
"box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.25); border-radius: 3px;" +
"padding: 2em; display: inline-block }" +
"#cv-revdel table { margin: 1em 0 }" +
"#cv-revdel td { padding: 0 0.5em }" +
"#cv-revdel a.disabled {color: gray;text-decoration: line-through;font-style: italic;}" +
"#cv-rd-submit { margin-right: 1em }"+
"#cv-rd-urls input { width: 50%; min-width: 30em; }"+
"#cv-rd-urls div { margin-bottom: 0.35em; }"+
"#cv-revdel div.bottom-row { margin-top: 1em; }"+
"#cv-rd-add-range { margin-right: 0.5em; }"
);
// Add the panel itself
var panel = document.createElement( "div" );
panel.id = "cv-revdel";
var urlHTML = "<div id='cv-rd-urls'>" +
"<div><label for='cv-rd-url0'>URL: </label><input type='text' "+
"id='cv-rd-url0' class='mw-ui-input mw-ui-input-inline'/>"+
"<button class='mw-ui-button mw-ui-quiet'>Remove</button></div>" +
"</div>"+
"<button id='cv-rd-url-add' class='mw-ui-button'>Add another</button>";
panel.innerHTML = "<p>Instructions: Select a range of revisions using " +
"the radio buttons you would normally use for comparing two " +
"revisions, then click 'Add range to revdel template' to add the " +
"range to the template. 'Include end?' should be unchecked to tag " +
"only the single revision in the 'Start' column. Fill in URLs, and " +
"then click 'Submit' to place the template.</p><br />" +
urlHTML + "<table id='cv-rd-ranges'><tr><th>Start</th><th>End</th>" +
"<th>Include end?</th><th>Remove</th></table>" +
"<input type='checkbox' id='cv-rd-cclean' />" +
"<label for='cv-rd-cclean'>Add {{subst:<a href='" + 
  mw.util.getUrl( "Template:" + CCLEAN_TPL ) + "'>Cclean</a>}} to " +
  ( ( mw.config.get( "wgNamespaceNumber" ) % 2 ) ? "this" : "talk" ) +
  " page</label><br />"+
"<div class='bottom-row'><button id='cv-rd-submit' class='mw-ui-button"+
  " mw-ui-progressive'>Submit</button>" +
  "<button id='cv-rd-close' class='mw-ui-button mw-ui-quiet'>Close</button>"+
  "<span id='cv-rd-status'></span></div>";
document.getElementById( "bodyContent" ).insertBefore( panel,
document.getElementById( "mw-content-text" ) );
// Add range-add buttons before each of the buttons
// that say "Compare selected revisions"
var cmpSelRevsBtns = document.getElementsByClassName( "historysubmit" );
for( var i = 0, n = cmpSelRevsBtns.length; i < n; i++ ) {
var rangeBtn = document.createElement( "button" );
rangeBtn.textContent = "Add range to revdel template";
rangeBtn.className = "mw-ui-button + mw-ui-progressive";
rangeBtn.id = "cv-rd-add-range"
cmpSelRevsBtns[i].parentNode.insertBefore( rangeBtn, cmpSelRevsBtns[i] );
rangeBtn.addEventListener( "click", function ( evt ) {
evt.preventDefault();
var oldidStart = document.querySelector( "li.selected.after" ).dataset.mwRevid;
var oldidEnd = document.querySelector( "li.selected.before" ).dataset.mwRevid;
// Add new row to ranges table
var rangesTable = document.getElementById( "cv-rd-ranges" ).getElementsByTagName( "tbody" )[0];
var newRow = rangesTable.insertRow( rangesTable.rows.length );
newRow.insertCell( 0 ).appendChild( makeOldidLink( oldidStart ) );
newRow.insertCell( 1 ).appendChild( makeOldidLink( oldidEnd ) );
newRow.insertCell( 2 ).innerHTML = "<input type='checkbox' />";
newRow.cells[2].childNodes[0].checked = true;
newRow.cells[2].childNodes[0].addEventListener( "click", function () {
this.parentNode.previousElementSibling.childNodes[0].className = this.checked ? "" : "disabled";
} );
var deleteBtn = document.createElement( "button" );
deleteBtn.textContent = "Delete";
deleteBtn.className = "delete";
deleteBtn.addEventListener( "click", function () {
this.parentNode.parentNode.parentNode.removeChild(
this.parentNode.parentNode );
} );
newRow.insertCell( 3 ).appendChild( deleteBtn );
} );
}
// Panel submission handler
document.getElementById( "cv-rd-submit" ).addEventListener( "click", function () {
$( this ).prop( "disabled", true );
$( "#cv-rd-status" ).empty();
var urls = Array.prototype.map.call( document.getElementById( "cv-rd-urls" ).children,
function ( e ) { return e.children[1].value; } );
var deferreds = [ addCvRevdel( urls ) ];
if( document.getElementById( "cv-rd-cclean" ).checked ) {
deferreds.push( addCclean( urls ) );
}
$.when.apply( $, deferreds ).then( function () {
// Return to content page
document.querySelector( "#ca-view a" ).click();
}, function () {
$( this ).prop( "disabled", false );
}.bind( this ) );
} );
// "Add URL" handler
document.getElementById( "cv-rd-url-add" ).addEventListener( "click", function () {
var numUrls = document.querySelectorAll( "#cv-rd-urls div" ).length;
if( numUrls < 3 ) {
var newDiv = document.createElement( "div" );
newDiv.innerHTML = "<label for='cv-rd-url" + urlCounter + "'>URL: </label>"+
"<input type='text' id='cv-rd-url" + urlCounter +
"' class='mw-ui-input mw-ui-input-inline'/>"+
"<button class='mw-ui-button mw-ui-quiet'>Remove</button>";
document.getElementById( "cv-rd-urls" ).appendChild( newDiv );
urlCounter++;
this.disabled = numUrls >= 2;
}
} );
// Remove URL handler
document.getElementById( "cv-rd-urls" ).addEventListener( "click", function ( e ) {
var numUrls = document.querySelectorAll( "#cv-rd-urls div" ).length;
if( e.target && e.target.nodeType === 1 && numUrls > 1 &&
e.target.tagName.toLowerCase() === "button" ) {
this.removeChild( document.getElementById( e.target.previousElementSibling.getAttribute( "id" ) ).parentNode );
document.getElementById( "cv-rd-url-add" ).disabled = false;
}
} );
// Close handler
document.getElementById( "cv-rd-close" ).addEventListener( "click", function () {
$( "#cv-revdel" ).remove();
$( ".cv-rd-add-range" ).remove();
} );
document.querySelector( "#cv-rd-urls input" ).focus();
}
mw.loader.using( [ "mediawiki.api", "mediawiki.util" ], function () {
importStylesheet( "User:Enterprisey/mw-ui-button.css" );
importStylesheet( "User:Enterprisey/mw-ui-input.css" );
pageName = mw.config.get( "wgPageName" );
if( mw.config.get( "wgAction" ) == "history" ) {
var link = mw.util.addPortletLink( "p-cactions", "#", "Request CV revdel", "pt-cv-revdel" );
link.addEventListener( "click", load );
if( mw.util.getParamValue( "open_cv_revdel" ) === "true" ) {
load();
}
} else if( mw.config.get( "wgNamespaceNumber" ) >= 0 ) {
var historyPage = mw.util.getUrl( pageName, { "action": "history", "open_cv_revdel": "true" } );
var link = mw.util.addPortletLink( "p-cactions", historyPage, "Request CV revdel", "pt-cv-revdel" );
}
} );
} );
// </nowiki>

/* Guywan/Scripts/ConfirmLogout.js */
// [[Category:Wikipedia scripts]]
// <nowiki>
$(() =>
{
var logout = $("#pt-logout a")[0];
if(!logout) return;
// Create new logout link.
var new_logout = document.createElement("a");
new_logout.innerText = logout.innerText;
// Insert new logout link and remove old.
logout.insertAdjacentElement("afterend", new_logout);
logout.remove();
// Get user-defined styling.
var fw = window.us_conlog_font_weight ? window.us_conlog_font_weight : "bold";
var fs = window.us_conlog_font_size ? window.us_conlog_font_size : "86%";
var bg = window.us_conlog_background ? window.us_conlog_background : "#FFDBDB";
var fg = window.us_conlog_color ? window.us_conlog_color : "#000";
var bd = window.us_conlog_border ? window.us_conlog_border : "2px solid #BB7070";
// Add styling to the page.
document.getElementsByTagName("body")[0].insertAdjacentHTML("afterbegin",
"<style>#use-conlog button { margin: 0 0.2em; border-radius: 9em; padding: 0 0.7em; box-sizing: border-box;" +
`font-weight: ${fw}; font-size: ${fs}; background: ${bg}; color: ${fg}; border: ${bd}}` +
"#use-conlog button:active { background:rgba(255,255,255,0.6)}</style>");
// Main grouping div for conlog elements.
document.getElementsByTagName("body")[0].insertAdjacentHTML("afterbegin", "<div id='use-conlog'></div>");
new_logout.addEventListener("click", event =>
{
// Create the logout confirmation prompt.
var conlog = document.getElementById("use-conlog");
conlog.insertAdjacentHTML("beforeend",
"<div onclick='this.style.transform = \"translateY(-130%)\";setTimeout(function(){this.remove()}.bind(this), 500);' style='" +
"position:fixed; top:0; left:0; right:0; margin: 0 0 auto 0; height: auto; line-height: 1.4em; " +
"padding: 0.6em 2em; opacity: 1; text-align: center; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.2); " +
"transform: translateY(-130%); transition: all 0.2s;" +
`font-weight: ${fw}; font-size: ${fs}; background: ${bg}; color: ${fg}; border: ${bd}'>` +
"<div style='margin-bottom: 0.5em;'>You clicked on a log-out link. Do you want to continue?</div>" +
"<div><button>No</button><button id='conlog-logout'>Log out</button></div></div>"
);
// Logout handler.
document.getElementById("conlog-logout").addEventListener("click", event =>
{
new mw.Api().post(
{
"action": "logout",
"token": mw.user.tokens.get("csrfToken")
})
.fail(result => { mw.notify("Failed to log out: " + result); })
.done(() => { window.location.href = mw.util.getUrl(mw.config.get("wgPageName")); });
});
// The logout confirmation prompt pops down from the top.
setTimeout(() => { conlog.lastChild.style.transform = "translateY(0%)"; }, 10);
event.preventDefault();
event.stopPropagation();
});
});
// </nowiki>

/* PleaseStand/userinfo.js */
// based on http://en.wikipedia.org/wiki/User:Fran Rogers/dimorphism.js
// and on http://en.wikipedia.org/wiki/User:Splarka/sysopdectector.js
function UserinfoJsFormatQty(qty, singular, plural) {
return String(qty).replace(/\d{1,3}(?=(\d{3})+(?!\d))/g, "$&,") + "\u00a0" + (qty == 1 ? singular : plural);
}
function UserinfoJsFormatDateRel(old) {
// The code below requires the computer's clock to be set correctly.
var age = new Date().getTime() - old.getTime();
var ageNumber, ageRemainder, ageWords;
if(age < 60000) {
// less than one minute old
ageNumber = Math.floor(age / 1000);
ageWords = UserinfoJsFormatQty(ageNumber, "second", "seconds");
} else if(age < 3600000) {
// less than one hour old
ageNumber = Math.floor(age / 60000);
ageWords = UserinfoJsFormatQty(ageNumber, "minute", "minutes");
} else if(age < 86400000) {
// less than one day old
ageNumber = Math.floor(age / 3600000);
ageWords = UserinfoJsFormatQty(ageNumber, "hour", "hours");
ageRemainder = Math.floor((age - ageNumber * 3600000) / 60000);
} else if(age < 604800000) {
// less than one week old
ageNumber = Math.floor(age / 86400000);
ageWords = UserinfoJsFormatQty(ageNumber, "day", "days");
} else if(age < 2592000000) {
// less than one month old
ageNumber = Math.floor(age / 604800000);
ageWords = UserinfoJsFormatQty(ageNumber, "week", "weeks");
} else if(age < 31536000000) {
// less than one year old
ageNumber = Math.floor(age / 2592000000);
ageWords = UserinfoJsFormatQty(ageNumber, "month", "months");
} else {
// one year or older
ageNumber = Math.floor(age / 31536000000);
ageWords = UserinfoJsFormatQty(ageNumber, "year", "years");
ageRemainder =
Math.floor((age - ageNumber * 31536000000) / 2592000000);
if(ageRemainder) {
ageWords += " " +
UserinfoJsFormatQty(ageRemainder, "month", "months");
}
   }
   return ageWords;
}
// If on a user or user talk page, and not a subpage...
if((mw.config.get("wgNamespaceNumber") == 2 || mw.config.get("wgNamespaceNumber") == 3) && !(/\//.test(mw.config.get("wgTitle")))) {   
// add a hook to...
mw.loader.using( ['mediawiki.util'], function() { $(function(){
// Request the user's information from the API.
// Note that this is allowed to be up to 5 minutes old.
var et = encodeURIComponent(mw.config.get("wgTitle"));
$.getJSON(mw.config.get("wgScriptPath") + "/api.php?format=json&action=query&list=users|usercontribs&usprop=blockinfo|editcount|gender|registration|groups&uclimit=1&ucprop=timestamp&ususers=" + et + "&ucuser=" + et + "&meta=allmessages&amprefix=grouppage-&amincludelocal=1")
.done(function(query) {
// When response arrives extract the information we need.
if(!query.query) { return; } // Suggested by Gary King to avoid JS errors --PS 2010-08-25
query = query.query;
var user, invalid, missing, groups, groupPages={}, editcount, registration, blocked, gender, lastEdited;
try {
user = query.users[0];
invalid = typeof user.invalid != "undefined";
missing = typeof user.missing != "undefined";
groups = (typeof user.groups == "object") ? user.groups : [];
editcount = (typeof user.editcount == "number") ? user.editcount : null;
registration = (typeof user.registration == "string") ?
new Date(user.registration) : null;
blocked = typeof user.blockedby != "undefined";
gender = (typeof user.gender == "string") ? user.gender : null;
lastEdited = (typeof query.usercontribs[0] == "object") &&
(typeof query.usercontribs[0].timestamp == "string") ?
new Date(query.usercontribs[0].timestamp) : null;
for (var am=0; am<query.allmessages.length; am++) {
groupPages[query.allmessages[am]["name"].replace("grouppage-","")] = query.allmessages[am]["*"].replace("{{ns:project}}:","Project:");
}
} catch(e) {
return; // Not much to do if the server is returning an error (e.g. if the username is malformed).
}
// Format the information for on-screen display
var statusText = "";
var ipUser = false;
var ipv4User = false;
var ipv6User = false;
// User status
if(blocked) {
statusText += "<a href=\"" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:Log&amp;page=" + 
encodeURIComponent(mw.config.get("wgFormattedNamespaces")[2] + ":" + user.name) +
"&amp;type=block\">blocked</a> ";
}
if (missing) {
statusText += "username not registered";
} else if (invalid) {
ipv4User = mw.util.isIPv4Address(user.name);
ipv6User = mw.util.isIPv6Address(user.name);
ipUser = ipv4User || ipv6User;
if (ipv4User) {
statusText += "anonymous IPv4 user";
} else if (ipv6User) {
statusText += "anonymous IPv6 user";
} else {
statusText += "invalid username";
}
} else {
// User is registered and may be in a privileged group. Below we have a list of user groups.
// Only need the ones different from the software's name (or ones to exclude), though.
var friendlyGroupNames = {
// Exclude implicit user group information provided by MW 1.17 --PS 2010-02-17
'*': false,
'user': false,
'autoconfirmed': false,
'named': false,
sysop: "administrator",
accountcreator: "account creator",
'import': "importer",
transwiki: "transwiki importer",
'ipblock-exempt': "IP block exemption",
confirmed: "confirmed user",
abusefilter: "edit filter manager",
'abusefilter-helper': "edit filter helper",
autoreviewer: "autopatrolled user",
epcoordinator: "Education Program course coordinator",
epcampus: "Education Program campus volunteer",
epinstructor: "Education Program instructor",
eponline: "Education Program online volunteer",
filemover: "file mover",
'massmessage-sender': "mass message sender",
templateeditor: "template editor",
extendedconfirmed: "extended confirmed user",
extendedmover: "page mover",
'flow-bot': "Flow bot",
reviewer: "pending changes reviewer",
suppress: "oversighter",
patroller: "new page reviewer"
};
var friendlyGroups = [];
for(var i = 0; i < groups.length; ++i) {
var s = groups[i];
var t = friendlyGroupNames.hasOwnProperty(s) ? friendlyGroupNames[s] : s;
if (t) {
if (groupPages.hasOwnProperty(s)) {
friendlyGroups.push("<a href=\"/wiki/" + encodeURIComponent( groupPages[s] ) + "\">" + t + "</a>");
} else {
friendlyGroups.push(t);
}
}
}
switch(friendlyGroups.length) {
case 0:
// User not in a privileged group
// Changed to "registered user" by request of [[User:Svanslyck]]
// --PS 2010-05-16
// statusText += "user";
if(blocked) {
statusText += "user";
} else {
statusText += "registered user";
}
break;
case 1:
statusText += friendlyGroups[0];
break;
case 2:
statusText += friendlyGroups[0] + " and " + friendlyGroups[1];
break;
default:
statusText += friendlyGroups.slice(0, -1).join(", ") +
", and " + friendlyGroups[friendlyGroups.length - 1];
break;
}
}
// Registration date
if(registration) {
var firstLoggedUser = new Date("22:16, 7 September 2005"); // When the [[Special:Log/newusers]] was first activated
if(registration >= firstLoggedUser) {
statusText += ", <a href='" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:Log&amp;type=newusers&amp;dir=prev&amp;limit=1&amp;user=" +
et + "'>" + UserinfoJsFormatDateRel(registration) + "</a> old";
} else {
statusText += ", <a href='" + mw.config.get("wgScriptPath") +
"/index.php?title=Special:ListUsers&amp;limit=1&amp;username=" +
et + "'>" + UserinfoJsFormatDateRel(registration) + "</a> old";
}
}
// Edit count
if(editcount !== null) {
statusText += ", with " +
"<a href=\"//tools.wmflabs.org/xtools-ec/?user=" +
encodeURIComponent(user.name) +
"&amp;project=en.wikipedia.org&amp;uselang=en\">" +
UserinfoJsFormatQty(editcount, "edit", "edits") + "</a>";
}
// Prefix status text with correct article
if("AEIOaeio".indexOf(statusText.charAt(statusText.indexOf('>')+1)) >= 0) {
statusText = "An " + statusText;
} else {
statusText = "A " + statusText;
}
// Add full stop to status text
statusText += ".";
// Last edited --PS 2010-06-27
// Added link to contributions page --PS 2010-07-03
if(lastEdited) {
statusText += " Last edited <a href=\"" + mw.config.get("wgArticlePath").replace("$1", "Special:Contributions/" + encodeURIComponent(user.name)) + "\">" + UserinfoJsFormatDateRel(lastEdited) + " ago</a>.";
}
// Show the correct gender symbol
var fh = document.getElementById("firstHeading") ||
document.getElementById("section-0");
if(!fh) return; // e.g. Minerva user talk pages
// Add classes for blocked, registered, and anonymous users
var newClasses = [];
if(blocked) {
newClasses.push("ps-blocked");
}
if(ipUser) {
newClasses.push("ps-anonymous");
} else if(invalid) {
newClasses.push("ps-invalid");
} else {
newClasses.push("ps-registered");
}
fh.className += (fh.className.length ? " " : "") + groups.map(function(s) {
return "ps-group-" + s;
}).concat(newClasses).join(" ");
var genderSpan = document.createElement("span");
genderSpan.id = "ps-gender-" + (gender || "unknown");
genderSpan.style.paddingLeft = "0.25em";
genderSpan.style.fontFamily = '"Lucida Grande", "Lucida Sans Unicode", "sans-serif"';
genderSpan.style.fontSize = "75%";
var genderSymbol;
switch(gender) {
case "male": genderSymbol = "\u2642"; break;
case "female": genderSymbol = "\u2640"; break;
default: genderSymbol = ""; break;
}
genderSpan.appendChild(document.createTextNode(genderSymbol));
fh.appendChild(genderSpan);
// Now show the other information. Non-standard? Yes, but it gets the job done.
// Add a period after the tagline when doing so. --PS 2010-07-03
var ss = document.getElementById("siteSub");
if(!ss) {
ss = document.createElement("div");
ss.id = "siteSub";
ss.innerHTML = "From Wikipedia, the free encyclopedia";
var bc = document.getElementById("bodyContent");
bc.insertBefore(ss, bc.firstChild);
}
ss.innerHTML = '<span id="ps-userinfo">' + statusText + '</span> ' + ss.innerHTML + '.';
ss.style.display = "block";
});
}); });
}

/* Splarka/oldafd.js */
/* AFD age detector, version [0.0.2a]
Originally from: http://en.wikipedia.org/wiki/User:Splarka/oldafd.js
Notes:
* Stuff
*/
if(mw.config.get('wgCurRevisionId') != 0 && mw.config.get('wgNamespaceNumber') == 4 && (mw.config.get('wgPageName').indexOf('_for_deletion/') != -1 || mw.config.get('wgPageName').indexOf('_for_discussion/') != -1)) addOnloadHook(function() {
  var url = mw.config.get('wgScriptPath') + '/api.php?maxage=3600&smaxage=3600&action=query&indexpageids&prop=revisions&rvdir=newer&rvlimit=1&rvprop=timestamp|comment|user&format=json&callback=ageCheckAFDCB&pageids=' + mw.config.get('wgArticleId');
  mw.loader.load(url);
});
function ageCheckAFDCB(obj) {
  var sub = document.getElementById('contentSub') || document.getElementById('topbar');
  var div = document.createElement('div');
  sub.appendChild(div);
  if(!obj['query'] || !obj['query']['pages'] || obj['query']['pages'].length == 0 || !obj['query']['pageids'] || obj['query']['pageids'].length == 0 || obj['error']) {
div.appendChild(document.createTextNode('Api error in AFD Age Detector.'));
return;
  }
  var id = obj['query']['pageids'][0];
  var page = obj['query']['pages'][id];
  var rev = page['revisions'][0];
  if(!rev || !rev['timestamp'] || !rev['user']) return
  var now = new Date();
  var tsd = new Date();
  tsd.setISO8601(rev['timestamp']);
  var timesince = Math.floor((now - tsd)/1000);
  if(timesince == '') timesince = -1
  var revinfo = 'Page created: ' + duration(timesince) + ' ago by ' + rev['user'];
  if(rev['comment']) div.setAttribute('title',rev['comment'])
  if(!rev['comment'] || rev['comment'].indexOf('Created') == -1) div.style.color = '#ff0000'
  div.appendChild(document.createTextNode(revinfo + '.'));
  if(timesince > 604800) appendCSS('body {background:#ffaaff !important;}')
}
function duration(input,depth) {
  var num = input;
  var out = '';
  var s = num % 60; num = Math.floor(num / 60);
  var m = num % 60; num = Math.floor(num / 60);
  var h = num % 24; num = Math.floor(num / 24);
  var d = num % 7;  num = Math.floor(num / 7);
  var w = num % 52; num = Math.floor(num / 52);
  var y = num
  if(y > 0) out += y + 'yrs '
  if(y + w > 0) out += w + 'wks '
  if(y + w + d > 0) out += d + 'days '
  if(y + w + d + h > 0) out += h + 'hrs '
  if(y + w + d + h + m > 0) out += m + 'mins '
  out += s + 'secs';  
  if(depth && depth < out.split(' ').length) {
out = out.split(' ').slice(0,depth).join(' ');
  }
  return out;
}
//ISO 8601 date module by Paul Sowden, licensed under AFL.
Date.prototype.setISO8601 = function(string) {
  if(!string) return
  var regexp = '([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?';
  var d = string.match(new RegExp(regexp));
  if(d.length < 1) return
  var offset = 0;
  var date = new Date(d[1], 0, 1);
  if(d[3]) date.setMonth(d[3] - 1)
  if(d[5]) date.setDate(d[5])
  if(d[7]) date.setHours(d[7])
  if(d[8]) date.setMinutes(d[8])
  if(d[10]) date.setSeconds(d[10])
  if(d[12]) date.setMilliseconds(Number('0.' + d[12]) * 1000)
  if(d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= ((d[15] == '-') ? 1 : -1);
  }
  offset -= date.getTimezoneOffset();
  time = (Number(date) + (offset * 60 * 1000));
  this.setTime(Number(time));
}

/* Novem Linguae/Scripts/NotSoFast.js */
// <nowiki>
/*
In [[Special:NewPagesFeed]], this script highlights articles newer than 15 minutes RED, and articles newer than 1 hour YELLOW. This is to remind you not to patrol those articles yet. [[WP:NPP]] says that articles should not be patrolled until at least 15 minutes have elapsed. This is to give the writer time to work on the article without getting CSD tagged, maintenance tagged, edit conflicted, etc.
*/
$(function() {
function checkAndHighlight(obj) {
let currentTimestamp = Math.floor(Date.now() / 1000);
let fifteenMinutesAgo = currentTimestamp - 60*15;
let oneHourAgo = currentTimestamp - 60*60;
$(obj).find('.mwe-pt-creation-date').each(function(i) {
let dateTimeString = $(this).html().trim(); // example: 00:34, 5 March 2022
dateTimeString = convertNPPDateTimeToJSDateTime(dateTimeString); // example: 5 March 2002 00:34
let milliseconds = Date.parse(dateTimeString);
oldTimeStamp = millisecondsToSeconds(milliseconds);
let browserTimeZoneOffset = new Date().getTimezoneOffset();
let mediaWikiTimeZone = mw.user.options.get('timecorrection');
newTimeStamp = convertTimeZoneFromMediaWikiToBrowser(
oldTimeStamp,
mediaWikiTimeZone,
browserTimeZoneOffset
);
if ( newTimeStamp > fifteenMinutesAgo ) {
$(this).css("background-color", "#F79C8F");
} else if ( newTimeStamp > oneHourAgo ) {
$(this).css("background-color", "#F5F591");
}
});
}
function convertTimeZoneFromMediaWikiToBrowser(timestamp, medaWikiTimeZoneString, browserTimeZoneOffset) {
let mediaWikiTimeZoneOffsetInMinutes = getMediaWikiTimeZoneOffset(medaWikiTimeZoneString);
let mediaWikiTimeZoneOffsetInSeconds = mediaWikiTimeZoneOffsetInMinutes * 60;
let browserTimeZoneOffsetInSeconds = browserTimeZoneOffset * 60;
let conversion = parseInt(mediaWikiTimeZoneOffsetInSeconds) - parseInt(browserTimeZoneOffsetInSeconds);
return parseInt(timestamp) + conversion;
}
/**
  * Converts a MediaWiki mw.user.options.get('timecorrection') from something like 'ZoneInfo|-420|America/Los_Angeles' or 'System|0' to -420 or 0.
  */
function getMediaWikiTimeZoneOffset(string) {
return parseInt(string.match(/\d+/)[0]);
}
/**
  * Returns the pagename, including the namespace name, but with spaces replaced by underscores
  */
function getArticleName() {
return mw.config.get('wgPageName');
}
/**
  * Flips date and time, so that Date.parse() recognizes it. Example: 00:34, 5 March 2022 becomes 5 March 2002 00:34
  */
function convertNPPDateTimeToJSDateTime(nppDateTime) {
return nppDateTime.replace(/(^.*), (.*)$/, '$2 $1');
}
function millisecondsToSeconds(milliseconds) {
return milliseconds / 1000;
}
function inAFCMode() {
return $('#mwe-pt-radio-afc').is(':checked');
}
let title = getArticleName();
if ( title != 'Special:NewPagesFeed' ) return;
// then run it again whenever a DOM node is inserted (the list refreshes as you scroll down, so this can be anytime you scroll down). could also be because this script loads BEFORE the the NPP applet (race condition)
new MutationObserver(() => {
if ( ! inAFCMode() ) {
checkAndHighlight(this);
}
}).observe($('#mwe-pt-list-view')[0], {childList: true});
// run it once in case the NPP applet loaded BEFORE this script (race condition)
$('.mwe-pt-list-item').each(function() {
if ( ! inAFCMode() ) {
checkAndHighlight(this);
}
});
});
/* Nardog suggestions:
~~1) switch to MutationObserver,~~
2) handle time zones,
3) use console.log to check for code execution instead of breakpoints,
4) "I see no reason to look for .mwe-pt-list-item btw. I would just query .mwe-pt-creation-date directly and give them a class once they're processed, and exclude them in the query (e.g. $('.mwe-pt-creation-date:not(.notsofast-processed)').each(function(){)"
*/
// </nowiki>

/* Novem Linguae/Scripts/DetectG4G5.js */
// <nowiki>
/*
- Displays an alert if an article may be a CSD G4 (previous AFD) or CSD G5 (created by a sockpuppet)
- Useful for new page patrolling
- Only runs on pages that have not been marked as reviewed
*/
// TODO: Code review. Is there a way to reduce the # of API queries?
$(async function() {
async function getWikicode(title) {
if ( ! mw.config.get('wgCurRevisionId') ) return ''; // if page is deleted, return blank
var wikicode = '';
title = encodeURIComponent(title);
await $.ajax({
url: 'https://en.wikipedia.org/w/api.php?action=parse&page='+title+'&prop=wikitext&formatversion=2&format=json',
success: function (result) {
wikicode = result['parse']['wikitext'];
},
dataType: "json",
async: false
});
return wikicode;
}
async function hasAFDTemplate(title) {
let wikicode = await getWikicode(title);
return wikicode.indexOf('{{Article for deletion') !== -1;
}
function displayWarning(html) {
$('#contentSub').before(`<div class="DetectG4G5" style="background-color: red">${html}</div>`);
}
async function isReviewed(pageID) {
let api = new mw.Api();
let response = await api.get( {
action: 'pagetriagelist',
format: 'json',
page_id: pageID,
} );
// no result
if ( response.pagetriagelist.result !== 'success' || response.pagetriagelist.pages.length === 0 ) {
return true;
// 1, 2, or 3
} else if ( parseInt(response.pagetriagelist.pages[0].patrol_status) > 0 ) {
return true;
// 0
} else {
return false;
}
}
async function afdExists(title) {
title = 'Wikipedia:Articles_for_deletion/' + title;
return await pageExists(title);
}
async function pageExists(title) {
let api = new mw.Api();
let response = await api.get( {
action: 'query',
format: 'json',
prop: 'revisions',
titles: title,
} );
let exists = typeof response.query.pages['-1'] === 'undefined';
return exists;
}
async function isBlocked(username) {
let api = new mw.Api();
let response = await api.get( {
action: "query",
format: "json",
list: "users",
usprop: "blockinfo",
ususers: username,
} );
let isBlocked = typeof response.query.users[0].blockid !== 'undefined';
return isBlocked;
}
async function isGloballyLocked(username) {
let api = new mw.Api();
let response = await api.get( {
action: 'query',
list: 'globalallusers',
agulimit: '1',
agufrom: username,
aguto: username,
aguprop: 'lockinfo',
} );
let isLocked = response.query.globalallusers.length !== 0 && response.query.globalallusers[0].locked === '';
return isLocked;
}
function getFirstValueInObject(obj) {
return obj[Object.keys(obj)[0]];
}
async function getPageCreator(title) {
let api = new mw.Api();
let response = await api.get( {
"action": "query",
"format": "json",
"prop": "revisions",
"titles": title,
"rvlimit": "1",
"rvdir": "newer"
} );
let page = getFirstValueInObject(response.query.pages);
let pageCreator = page.revisions[0].user;
return pageCreator;
}
function shouldRunOnThisPage() {
// don't run when not viewing articles
let action = mw.config.get('wgAction');
if ( action != 'view' ) return false;
// don't run when viewing diffs
let isDiff = mw.config.get('wgDiffNewId');
if ( isDiff ) return false;
let isDeletedPage = ( ! mw.config.get('wgCurRevisionId') );
if ( isDeletedPage ) return false;
// Only run in mainspace
let namespace = mw.config.get('wgNamespaceNumber');
let title = mw.config.get('wgPageName'); // includes namespace, underscores instead of spaces
if ( namespace !== 0 && title != 'User:Novem_Linguae/sandbox' ) return false;
return true;
}
if ( ! shouldRunOnThisPage() ) return;
let title = mw.config.get('wgPageName'); // includes namespace, underscores instead of spaces
let pageID = mw.config.get('wgArticleId');
if ( await isReviewed(pageID) ) {
return;
}
if ( await afdExists(title) && ! await hasAFDTemplate(title) ) {
let href = mw.config.get('wgArticlePath').replace('$1', 'Wikipedia:Articles_for_deletion/' + title);
displayWarning(`<span style="font-weight:bold">CSD G4:</span> There is an <a href="${href}">AFD page</a> for this article. It may qualify for CSD G4.`);
}
let pageCreator = await getPageCreator(title);
if ( await isBlocked(pageCreator) ) {
displayWarning('<span style="font-weight:bold">CSD G5:</span> The creator of this page is blocked. This article may qualify for CSD G5.');
}
if ( await isGloballyLocked(pageCreator) ) {
displayWarning('<span style="font-weight:bold">CSD G5:</span> The creator of this page is globally locked. This article may qualify for CSD G5.');
}
});
// </nowiki>

/* Writ_Keeper/Scripts/deletionFinder.js */
$(document).ready(deletionChecker);
encodedTitle = encodeURIComponent(mw.config.get("wgPageName"));
function deletionChecker()
{
var encodedTitle = encodeURIComponent(mw.config.get("wgPageName"));
var delRequest = $.get("/w/api.php?action=query&list=logevents&format=json&leprop=ids&letype=delete&letitle=" + encodedTitle + "&lelimit=1", null, delCallback, "json");
if(mw.config.get("wgCanonicalNamespace") == "")
{
var afdRequest = $.get("/w/api.php?action=query&list=allpages&format=json&apfrom=Articles%20for%20deletion%2F" + encodedTitle + "&apto=Articles%20for%20deletion%2F" + encodedTitle + "&apnamespace=4&aplimit=1", null, afdCallback, "json");
}
}
function delCallback(delResults)
{
if(delResults.query.logevents[0] != null)
{
/*
var searchNode = document.createElement("a");
searchNode.href = mw.config.get("wgServer") + "/w/index.php?title=Special%3ALog&type=delete&page=" + encodedTitle;
searchNode.target = "_blank";
searchNode.innerHTML = "<span style='font-size:x-small;'>prev dels</span>";
document.getElementById("firstHeading").appendChild(searchNode);
*/
var searchNode = " <a id='prevDelsLink' target='_blank'><span style='font-size:x-small;'>prev dels</span></a>"
$("#firstHeading").append(searchNode);
$("#prevDelsLink").attr("href", mw.config.get("wgServer") + "/w/index.php?title=Special%3ALog&type=delete&page=" + encodedTitle);
}
}
function afdCallback(afdResults)
{
if(afdResults.query.allpages[0] != null)
{
/*
var searchNode = document.createElement("a");
searchNode.href = mw.config.get("wgServer") + "/w/index.php?title=Special%3AAllPages&from=Articles+for+deletion%2F" + encodedTitle + "&to=Articles+for+deletion%2F" + encodedTitle + "+%289z%29&namespace=4";
searchNode.target = "_blank";
searchNode.innerHTML = "<span style='font-size:x-small;'>prev AfDs</span>";
document.getElementById("firstHeading").appendChild(searchNode);
*/
var searchNode = " <a id='prevAFDsLink' target='_blank'><span style='font-size:x-small;'>prev AfDs</span></a>"
$("#firstHeading").append(searchNode);
$("#prevAFDsLink").attr("href", mw.config.get("wgServer") + "/w/index.php?title=Special%3AAllPages&from=Articles+for+deletion%2F" + encodedTitle + "&to=Articles+for+deletion%2F" + encodedTitle + "+%289z%29&namespace=4");
}
}

/* DannyS712/SATG.js */
//Written by Abelmoschus Esculentus
//Date: 12 January 2019
//Copied from [[User:Abelmoschus Esculentus/SATG.js]
//<nowiki>
function screen2(number) {
  var source = ["placeholder"], ind = ["placeholder"], ind_just = ["placeholder"], rel = ["placeholder"], rel_just = ["placeholder"], sig = ["placeholder"], sig_just = ["placeholder"];
$('.SATG-source').each(function () {
source.push(this.value);
  });
  $('.SATG-selecti').each(function () {
ind.push(this.value);
  });
  $('.SATG-justi').each(function () {
ind_just.push(this.value);
  });
  $('.SATG-selectr').each(function () {
rel.push(this.value);
  });
  $('.SATG-justr').each(function () {
rel_just.push(this.value);
  });
  $('.SATG-selects').each(function () {
sig.push(this.value);
  });
  $('.SATG-justs').each(function () {
sig_just.push(this.value);
  });
  var output = "{{ source assess table |\n";
  for (var i = 1; i <= number; i++) {
output += "{{ source assess\n";
output += "| source   = " + source[i] + "\n";
output += "| ind  = " + ind[i] + "\n";
output += "| ind_just = " + ind_just[i] + "\n";
output += "| rel  = " + rel[i] + "\n";
output += "| rel_just = " + rel_just[i] + "\n";
output += "| sig  = " + sig[i] + "\n";
output += "| sig_just = " + sig_just[i] + "\n";
output += "}}\n";
  }
  output += "}}";
$("#SATG-interface-content").css({
  "min-height": "7em",
  "width" : "875px",
  "height" : "400px",
  "overflow-y": "hidden"
});
$("#SATG-interface-content").empty();
$("#SATG-interface-content").text('Generating...');
$('.SATG-tip').remove();
$("#SATG-interface-content").empty();
$("#SATG-interface-content").append(
$('<button>').attr('id','SATG-copy').text('Copy'),
$('<label>').attr('id','SATG-copied').text(''),
$('<textarea>').css({'resize':'none'}).attr({'id':'SATG-output','readonly':'true','rows':'20','cols':'35'}).text(output)
);
$('#SATG-copy').click(function() {
var copy = document.getElementById("SATG-output");
copy.select();
document.execCommand("copy");
$('#SATG-copied').text(' Copied to your clipboard!');
});
}
function screen1(number) {
if ($("#SATG-interface-content").text() == "Loading form...") {
$("#SATG-interface-content").empty();
}
$("#SATG-interface-footer").append(
$('<button>').attr('id', 'SATG-back').css('margin-left','1em').text('Back')
);
$("#SATG-interface-footer").prepend(
$('<small>').attr('class','SATG-tip').text('**Only available for options "Yes", "No", "Partially" and "Unknown"'),
$('<br>').attr('class','SATG-tip')
);
$('#SATG-back').click(function () {
$('#SATG-back').remove();
$("#SATG-interface-content").css({
"min-height": "7em",
"width" : "875px",
"height" : "400px",
"overflow-y": "scroll"
});
screen0();
});
/*var arr = [
{val : 1, text: 'Yes'},
{val : 2, text: 'No'},
{val : 3, text: 'Partially'},
{val : 4, text: 'Unknown'},
{val : 5, text: 'None'}
];*/
for (var i = 1; i <= number; i++) {
if (i != 1) {
$("#SATG-interface-content").append('<hr>');
}
$("#SATG-interface-content").append(
$('<div>').css('margin-bottom','0.5em').append(
$('<label>').text('Source '+i+': '),
$('<input>').attr({'type':'text','class':'SATG-source'})
),
  $('<div>').css('margin-bottom','0.5em').append(
 $('<label>').text('Independent? '),
 $('<select>').attr('class','SATG-selecti')
.append($("<option>").attr('value','y').text('Yes'))
.append($("<option>").attr('value','n').text('No'))
.append($("<option>").attr('value','-').text('Partially'))
.append($("<option>").attr('value','?').text('Unknown'))
.append($("<option>").attr({'value':'','selected':'true'}).text('None')),
 $('<label>').text(' **Justification: '),
 $('<input>').attr({'type':'text','class':'SATG-justi'})
  ),
  $('<div>').css('margin-bottom','0.5em').append(
$('<label>').text('Reliable? '),
$('<select>').attr('class','SATG-selectr')
 .append($("<option>").attr('value','y').text('Yes'))
 .append($("<option>").attr('value','n').text('No'))
 .append($("<option>").attr('value','-').text('Partially'))
 .append($("<option>").attr('value','?').text('Unknown'))
 .append($("<option>").attr({'value':'','selected':'true'}).text('None')),
$('<label>').text(' **Justification: '),
$('<input>').attr({'type':'text','class':'SATG-justr'})
 ),
 $('<div>').css('margin-bottom','0.5em').append(
  $('<label>').attr('id','SATG-labels-'+i).text('Significant coverage? '),
$('<select>').attr('class','SATG-selects')
.append($("<option>").attr('value','y').text('Yes'))
.append($("<option>").attr('value','n').text('No'))
.append($("<option>").attr('value','-').text('Partially'))
.append($("<option>").attr('value','?').text('Unknown'))
.append($("<option>").attr({'value':'','selected':'true'}).text('None')),
$('<label>').text(' **Justification: '),
$('<input>').attr({'type':'text','class':'SATG-justs'})
  )
);
}
$("#SATG-interface-content").append(
$('<button>').attr('id','SATG-generate').text('Generate!')
);
$('#SATG-generate').click(function() {
screen2(number);
});
}
function satg_init() {
  mw.util.addPortletLink('p-tb', 'javascript:void(0)', 'SA Table Generator', 'aca-satg', null, null);
  $('#aca-satg').on('click', function() {
$('body').prepend('<div id="SATG-modal">'+
  '<div id="SATG-interface">'+
'<h4 id="SATG-interface-header"></h4>'+
'<hr>'+
'<div id="SATG-interface-content"></div>'+
'<hr>'+
'<div id="SATG-interface-footer"></div>'+
  '</div>'+
'</div>');
$("#SATG-modal").css({
  "position": "fixed",
  "z-index": "1",
  "left": "0",
  "top": "0",
  "width": "100%",
  "height": "100%",
  "overflow": "hidden",
  "background-color": "rgba(0,0,0,0.4)"
});
$("#SATG-interface").css({
  "background-color": "#e8f0ff",
  "margin": "15% auto",
  "padding": "2px 20px",
  "border": "1px solid #888",
  "width": "80%",
  "max-width": "60em",
  "font-size": "90%"
});
$("#SATG-interface-content").css({
  "min-height": "7em",
  "width" : "875px",
  "height" : "400px",
  "overflow-y": "scroll"
});
$("#SATG-interface-footor").css("min-height", "3em");
screen0();
  });
}
var screen0 = function() {
  $("#SATG-interface-header, #SATG-interface-content, #SATG-interface-footer").empty();
  $("#SATG-interface-header").text("Source Assess Table Generator");
  $("#SATG-interface-content").append(
$('<div>').css('margin-bottom','0.5em').append(
  $('<label>').attr({'for':'SATG-userinput-label', 'id':'SATG-userinput-label'}).text('How many sources do you want to assess? '),
  $('<input>').attr({'type':'number', 'name':'SATG-userinput','id':'SATG-userinput','min':'1','max':'100','value':'1'}) //set limits
),
$('<div>').css('margin-bottom','0.5em').append(
  $('<button>').attr({'name':'SATG-userinput-button', 'id':'SATG-userinput-button'}).text('Load')
)
  );
  $("#SATG-interface-footer").append(
$('<button>').attr('id', 'SATG-cancel').text('Close')
  );
  $('#SATG-cancel').click(function() {
$('#SATG-modal').remove();
  });
  $('#SATG-userinput-button').click(function() {
  var temp = $('#SATG-userinput').val();
  if (temp > 100 || temp < 1) {
  alert('Invalid value');
  }
  else {
  $('#SATG-interface-content').empty();
  $('#SATG-interface-content').text('Loading form...');
  screen1(temp);
  }
  });
};
mw.loader.using(['mediawiki.util'], function() {
  satg_init();
});
//</nowiki>

/* Jackmcbarn/editProtectedHelper.js */
// <nowiki>
var browserHasDataCorruptionBug = $('<div style="color:#ffd"></div>')[0].outerHTML != '<div style="color:#ffd"></div>';
if(browserHasDataCorruptionBug && ('undefined' === typeof ephAllowDataCorruptionBug || !ephAllowDataCorruptionBug)) {
$(document).ready(function() {
$('.editrequest .mbox-text').append('<small>The editProtectedHelper script is currently disabled because your browser has a bug which will cause corruption of pages that it edits with Parsoid. See <a href="https://www.mediawiki.org/wiki/User:Catrope/IE_bugs#style_attributes_are_aggressively_normalized.2C_causing_data_loss">here</a> for details. To override this, add "ephAllowDataCorruptionBug = true;" immediately above the line where you load this script. You are responsible for any damage to pages that this causes.</small>');
});
// only enable this on the latest revision of the page
} else if(mw.config.get('wgRevisionId') == mw.config.get('wgCurRevisionId')) {
$(document).ready(function() {
mw.loader.using( ['mediawiki.api'], function() {
'use strict';
var templateResponses = [
[ '', '(No template response)' ],
[ 'd', 'Done' ],
[ 'pd', 'Partly done:' ],
[ 'nd', 'Not done:' ],
[ 'nfn', 'Not done for now:' ],
[ 'c', 'Not done: please establish a consensus for this alteration before using the {{edit protected}} template.'] , // TODO make dynamic
[ 'rs', 'Not done: please provide reliable sources that support the change you want to be made.' ],
[ 'xy', 'Not done: it\'s not clear what changes you want made. Please mention the specific changes in a "change X to Y" format.' ],
[ 'mis', 'Not done: this is the talk page for discussing improvements to the template {{EP}}. Please make your request at the talk page for the article concerned.'], // TODO make dynamic
[ 'sb', 'Not done: please make your requested changes to the template\'s sandbox first; see WP:TESTCASES.' ],
[ 'tp', 'Not done: this is the talk page for discussing improvements to the template {{EP}}. If possible, please make your request at the talk page for the article concerned. If you cannot edit the article\'s talk page, you can instead make your request at Wikipedia:Requests for page protection#Current requests for edits to a protected page.' ],
[ 'a', 'Already done' ],
[ 'hr', 'Not done: According to the page\'s protection level and your user rights, you should be able to edit the page yourself. If you seem to be unable to, please reopen the request with further details.'],
[ 'nlp', 'Not done: The page\'s protection level has changed since this request was placed. You should now be able to edit the page yourself. If you still seem to be unable to, please reopen the request with further details.'],
[ 'doc', 'Not done: {{edit protected}} is usually not required for edits to the documentation, categories, or interlanguage links of templates using a documentation subpage. Use the \'edit\' link at the top of the green "Template documentation" box to edit the documentation subpage.' ],
[ 'drv', 'Not done: requests for recreating deleted pages protected against creation should be made at Wikipedia:Deletion review.' ],
[ 'r', 'Not done: requests for increases to the page protection level should be made at Wikipedia:Requests for page protection.' ],
[ 'ru', 'Not done: requests for decreases to the page protection level should be directed to the protecting admin or to Wikipedia:Requests for page protection if the protecting admin is not active or has declined the request.' ],
[ 'p', 'Not done: this is not the right page to request additional user rights. You may reopen this request with the specific changes to be made and someone will add them for you.'], // TODO make dynamic
[ 'm', 'Not done: page move requests should be made at Wikipedia:Requested moves.' ],
[ 'q', 'Question:' ],
[ 'note', 'Note:' ],
[ 'udp', 'Undone: This request (or the completed portion of it) has been undone.' ],
[ 'ud', 'Undone: This request has been undone.' ]
];
var selector = $('.editrequest .mbox-text');
// Global variable. In onParsoidDomReceived, this is set
// to the ETag header so that we can pass it back later
// in convertModifiedDom.
var gEtag;
// Global variable. In onParsoidDomReceived, this is
// set to the string we get back from the Parsoid API.
// Used in convertModifiedDom.
var parsoidDom;
var selectedLevel = { semi: [' selected="selected"', '', '', '', ''], extended: ['', ' selected="selected"', '', '', ''], template: ['', '', ' selected="selected"', '', ''], full: ['', '', '', ' selected="selected"', ''], interface: ['', '', '', '', ' selected="selected"'] };
var templateLevel = { semi: 'semi-', extended: 'extended-', template: 'template-', full: 'fully-', interface: 'interface-' };
var responseLevel = { semi: '{' + '{subst:ESp|', extended: '{' + '{subst:EEp|', template: '{' + '{subst:ETp|', full: '{' + '{subst:EP|', interface: '{' + '{subst:EIp|' };
var warnOnRespond = false, warnOnQuickRespond = true, warnOnRemove = true, autoFixLevel = true;
var quickResponses = [
[ 'd', '', 'Done'],
[ 'rs', '', 'Needs reliable sources'],
[ 'xy', '', 'Unclear/X to Y'],
[ 'hr', '', 'Could always edit'],
[ 'nlp', '', 'Can edit now'],
[ 'mis', '', 'Misplaced']
];
if('undefined' !== typeof ephWarnOnRespond) {
warnOnRespond = ephWarnOnRespond;
}
if('undefined' !== typeof ephWarnOnQuickRespond) {
warnOnQuickRespond = ephWarnOnQuickRespond;
}
if('undefined' !== typeof ephWarnOnRemove) {
warnOnRemove = ephWarnOnRemove;
}
if('undefined' !== typeof ephAutoFixLevel) {
autoFixLevel = ephAutoFixLevel;
}
if('undefined' !== typeof ephQuickResponses) {
quickResponses = ephQuickResponses;
}
function yesno(val, def) {
if(typeof val === 'string') {
val = val.toLowerCase();
}
if(typeof val === 'undefined' || val === '') {
return undefined;
} else if(val === 'yes' || val === 'y' || val === 'true' || val === 1) {
return true;
} else if(val === 'no' || val === 'n' || val === 'false' || val === 0) {
return false;
}
return def;
}
function getBanner(level, pagename, answered, force, demo) {
return '{{edit ' + templateLevel[level] + 'protected' + (pagename !== '' ? '|' + pagename : '') + '|answered=' + (answered ? 'yes' : 'no') + (force ? '|force=yes' : '') + (demo ? '|demo=yes}}' : '}}');
}
function getResponse(level, template, free) {
return ':' + (template === '' ? '' : responseLevel[level] + template + '}} ') + (free === '' ? '' : free + ' ') + '~~' + "~~\n";
}
function makeUniqueString(index) {
// this looks like a strip marker on purpose
return "\x7fUNIQ" + Math.random().toString(16).substr(2) + '-editProtectedHelper-' + index + "-QINU\x7f";
}
function saveWikitextFromParsoid(parsoidObj, data) {
var newWikitext, tmp, removerequest = false;
if(parsoidObj.removerequest) {
tmp = data.split(parsoidObj.templateMarker);
removerequest = true;
newWikitext = tmp[0];
if(parsoidObj.respondedInPage) {
tmp = tmp[1].split(parsoidObj.responseMarker);
newWikitext = newWikitext.replace(/\n+$/, '') + "\n\n" + tmp[1].replace(/^\n+/, '');
}
} else {
var response = getResponse(parsoidObj.form.level.value, parsoidObj.form.responsetemplate.value, parsoidObj.form.responsefree.value);
var banner = getBanner(parsoidObj.form.level.value, parsoidObj.form.pagetoedit.value, parsoidObj.form.answered.checked, parsoidObj.form.force.checked, parsoidObj.demo);
// prevent empty response
if(response == ':~~' + "~~\n") {
response = '';
}
tmp = data.split(parsoidObj.templateMarker);
newWikitext = tmp[0].replace(/\n*$/, tmp[0].trim().length ? "\n\n" : '') + banner + tmp[1].replace(/^\n*/, "\n");
if(parsoidObj.respondedInPage) {
tmp = newWikitext.split(parsoidObj.responseMarker);
newWikitext = tmp[0].replace(/\n*$/, "\n") + response + tmp[1].replace(/^\n*/, "\n");
} else {
newWikitext = newWikitext.replace(/\n*$/, "\n") + response;
}
}
var resultObj = parsoidObj.resultObj, sectionName = parsoidObj.section && parsoidObj.section.text().trim();
if(sectionName && (sectionName.indexOf(parsoidObj.templateMarker) !== -1 || sectionName.indexOf(parsoidObj.responseMarker) !== -1)) {
// someone put an edit request template inside the section header, or something like that.
// don't even try to include a working section link in that case
sectionName = null;
}
//console.log(newWikitext);
resultObj.text('Saving...');
debugger; // triggers only if debugger is active
new mw.Api().get( { action: 'query', prop: 'revisions', rvprop: 'timestamp', revids: mw.config.get('wgRevisionId') }).done(function(data) {
new mw.Api().postWithEditToken( { action: 'edit', pageid: mw.config.get('wgArticleId'), text: newWikitext, summary: (sectionName ? '/' + '* ' + sectionName + ' *' + '/ ' : '') + (removerequest ? 'Removed' : 'Responded to') + ' edit request', tags: 'editProtectedHelper', notminor: true, nocreate: true, basetimestamp: data.query.pages[mw.config.get('wgArticleId')].revisions[0].timestamp } ).done(function(result) {
if(typeof(result.edit.newrevid) === 'undefined' || typeof(result.edit.oldrevid) === 'undefined') {
resultObj.css('color', 'red').text("Error: The API response omitted required information. Please check the page's history manually to see whether your edit was saved properly. The console may contain details.");
} else {
resultObj.css('color', 'green').text('Success: Loading diff...');
if( window.editProtectedHelperReloadAfter ) {
window.location.reload( /* forcedReload */ true );
} else {
location.href = mw.config.get('wgScript') + '?diff=' + result.edit.newrevid + '&oldid=' + result.edit.oldrevid;
}
}
}).fail(function(err) {
resultObj.css('color', 'red').text('Error: ' + err + '. The console may contain details.');
}).always(console.log);
});
}
function convertModifiedDom(e) {
var parsoidObj, nextRequestBeforeHeader = false;
if(warnOnRespond && !confirm('Are you sure you want to respond to this edit request?')) {
return false;
}
$('.editrequest button').prop('disabled', true);
parsoidObj = e.data;
parsoidObj.resultObj.text('Preparing new wikitext...');
var editReqTpl = $(parsoidObj);
// Don't break about continuity - get to the first
// element of the tranclusion block
while (editReqTpl && editReqTpl.attr('data-mw') === undefined) {
editReqTpl = editReqTpl.prev();
}
if (!editReqTpl) {
console.log("Error 3: Unexpected error traversing DOM. Cannot save!");
return;
}
var error = false;
editReqTpl.before(parsoidObj.templateMarker);
$('h1,h2,h3,h4,h5,h6,.editrequest', parsoidDom).each(function() {
var obj = $(this);
if(!obj.hasClass('editrequest')) {
// obj is a heading
if(obj.closest('[typeof="mw:Transclusion"]').length) {
// it's from a template transclusion. ignore
return true;
}
if(obj.add(parsoidObj)[0] === this) {
// (section) heading shows up before the edit request banner.
// Set as our section header (tentatively) and otherwise ignore.
parsoidObj.section = obj;
return true;
}
} else {
// obj is a (potentially different) edit request.
if(obj.add(parsoidObj)[0] === this) {
// not after our edit request banner. ignore
return true;
} else {
nextRequestBeforeHeader = true;
}
}
if (obj.attr('about')) {
// Don't break about continuity - get to the first
// element of the tranclusion block
while (obj && obj.attr('data-mw') === undefined) {
obj = obj.prev();
}
if (!obj) {
error = true; // Don't know what happened here!
return false;
}
}
obj.before(parsoidObj.responseMarker);
parsoidObj.respondedInPage = true;
return false;
});
if (error) {
console.log("Error 4: Unexpected error traversing DOM. Cannot save!");
return;
}
// If the section header is immediately before a request being removed, remove it too
// Do this before removing the edit req (template)
if(parsoidObj.removerequest && !nextRequestBeforeHeader && editReqTpl.prev().is(parsoidObj.section)) {
parsoidObj.section.remove();
}
// Remove the edit req (template)
$('[about="' + $(parsoidObj).attr('about').replace('\\', '\\\\').replace('"', '\\"') + '"]', parsoidDom).remove();
$.ajax({
type: 'POST',
url: '/api/rest_v1/transform/html/to/wikitext/' + encodeURIComponent(mw.config.get('wgPageName')) + '/' + mw.config.get('wgRevisionId'),
headers: {
Accept: 'text/plain; charset=utf-8; profile="mediawiki.org/specs/wikitext/1.0.0"',
'Api-User-Agent': 'editProtectedHelper (https://en.wikipedia.org/wiki/User:Jackmcbarn/editProtectedHelper)',
'If-Match': gEtag
},
data: { html: parsoidDom.documentElement.outerHTML },
success: function(data) { return saveWikitextFromParsoid(parsoidObj, data); }
});
}
function appendForm(obj, level, pagename, answered, force, parsoidObj) {
if(browserHasDataCorruptionBug) {
$(obj).append('<div style="color:red;font-weight:bold">WARNING: Your browser has a bug which will cause corruption of pages that it edits with Parsoid. See <a href="https://www.mediawiki.org/wiki/User:Catrope/IE_bugs#style_attributes_are_aggressively_normalized.2C_causing_data_loss">here</a> for details. You are responsible for any damage to pages that this causes.</div>');
}
var form = $('<form class="editProtectedHelper" style="display: none" />');
form.append('<style scoped>.mw-ui-input { background-color: white; }</style>');
if(selectedLevel[level]) {
form.append('<label>Level: <select name="level" class="mw-ui-input mw-ui-input-inline"><option value="semi"' + selectedLevel[level][0] + '>Semi-protected</option><option value="extended"' + selectedLevel[level][1] + '>Extended-confirmed-protected</option><option value="template"' + selectedLevel[level][2] + '>Template-protected</option><option value="full"' + selectedLevel[level][3] + '>Fully protected</option><option value="interface"' + selectedLevel[level][4] + '>Interface-protected</option></select></label> ');
}
if(force) {
form.append('<label>Disable protection level autodetection (use only if necessary): <input type="checkbox" name="force" checked="checked" /></label> ' );
} else {
form.append('<input type="checkbox" name="force" style="display: none" />' ); // if this is off and you want to turn it on, do it with firebug or something. otherwise people will use this when they shouldn't
}
var label = $('<label>Page to be edited: </label>');
label.append($('<input type="text" name="pagetoedit" class="mw-ui-input mw-ui-input-inline" />').attr('value', pagename !== '<!-- Page to be edited -->' ? pagename : ''));
form.append(label);
form.append(' <label>Answered: <input type="checkbox" name="answered"' + (answered ? ' checked="checked"' : '') + ' /></label><br />Response: ');
var select = $('<select name="responsetemplate" class="mw-ui-input" />');
templateResponses.forEach(function(r) {
select.append($('<option />').attr('value', r[0]).text(r[1]));
});
form.append(select);
form.append('<textarea name="responsefree" class="mw-ui-input" placeholder="Enter any custom response text here. A signature will automatically be appended."></textarea> ~~' + '~~ ');
var resultObj = $('<span></span>');
var button = $('<button type="button" class="mw-ui-button mw-ui-constructive">Submit</button>').click(parsoidObj, convertModifiedDom);
form.append(button);
form.append(' ');
$(obj).append(form);
var buttons = $('<div />');
buttons.append($('<button type="button" class="mw-ui-button mw-ui-progressive">Respond</button>').click(function(){
buttons.hide();
$(obj).closest('.editrequest').removeClass('mbox-small');
form.show();
}))
.append(' ')
.append($('<button type="button" class="mw-ui-button mw-ui-destructive">Remove request</button>').click(function(){
if(!warnOnRemove || confirm('Are you sure you want to completely remove this edit request from the page? In general, this should not be done to good-faith requests unless they are blank or duplicates of other requests by the same user, etc.')) {
warnOnRespond = false;
parsoidObj.removerequest = true;
button.click();
}
}));
if(!answered) {
quickResponses.forEach(function(r){
buttons.append(' ').append($('<button type="button" class="mw-ui-button mw-ui-constructive" />').text(r[2]).click(function(){
if(!warnOnQuickRespond || confirm('Are you sure you want to respond to this edit request?')) {
warnOnRespond = false;
parsoidObj.form.answered.checked = true;
parsoidObj.form.responsetemplate.value = r[0];
parsoidObj.form.responsefree.value = r[1];
button.click();
}
}));
});
}
$(obj).append(buttons);
$(obj).append(resultObj);
parsoidObj.form = form[0];
parsoidObj.resultObj = resultObj;
}
function parsoidSetupFieldsForTemplate(index) {
var mboxObj = $(this);
var obj = mboxObj;
// Get to the first element of the tranclusion block
// to correctly recover 'data-mw'.
var aboutId = obj.attr('about');
if (aboutId === undefined) {
console.log("Error 1: No data-mw attribute was found on edit request banner " + index + ". This could be because some template above it on the page opened an HTML tag but didn't close it.");
return;
}
var data_mw = obj.attr('data-mw');
var done = data_mw !== undefined;
while (!done) {
obj = obj.prev();
if (!obj || obj.attr('about') !== aboutId) {
console.log("Error 2: The HTML seems broken. Either the script is broken and needs an update or this is a Parsoid bug.");
return;
}
data_mw = obj.attr('data-mw');
done = data_mw !== undefined;
}
data_mw = JSON.parse(data_mw);
var level = mboxObj.attr('data-origlevel');
if (autoFixLevel) {
switch(this.id) {
case 'editsemiprotected':
level = 'semi';
break;
case 'editextendedprotected':
level = 'extended';
break;
case 'edittemplateprotected':
level = 'template';
break;
case 'editprotected':
level = 'full';
break;
case 'editinterfaceprotected':
level = 'interface';
}
}
var params = [];
for(var key in data_mw.parts[0].template.params) {
params[key] = data_mw.parts[0].template.params[key].wt;
}
// this only runs on numerical parameters
params.forEach(function(value, key) {
if(/=/.test(value)) {
params[key] = key + '=' + value;
}
});
var pagename = params.join('|').replace(/^\|+|\|+$/g, '').replace(/\|+/g, '|');
var answered = yesno(params.ans || params.answered, true);
this.demo = yesno(params.demo);
var force = yesno(params.force);
this.params = params;
this.templateMarker = makeUniqueString(2 * index);
this.responseMarker = makeUniqueString(2 * index + 1);
appendForm(selector[index], level, pagename, answered, force, this);
}
function onParsoidDomReceived(data, textStatus, jqXHR) {
// Grab the ETag header and store it in the global
// gEtag for later use
var headers = jqXHR.getAllResponseHeaders().split( "\r\n" );
for( var i = 0, n = headers.length; i < n; i++ ) {
if( headers[i].substring( 0, headers[i].indexOf( ":" ) ) === "etag" ) {
gEtag = headers[i].substring( headers[i].indexOf( ":" ) + 2 );
break;
}
}
parsoidDom = new DOMParser().parseFromString(data, 'text/html');
if(!parsoidDom) {
// blech. Safari.
parsoidDom = document.implementation.createHTMLDocument('');
parsoidDom.documentElement.innerHTML = data;
}
var parsoidSelector = $('.editrequest', parsoidDom);
if(selector.length != parsoidSelector.length) {
console.log('Sanity check failed: ' + selector.length + ' edit requests exist in the page but only ' + parsoidSelector.length + ' were found in Parsoid\'s output.');
return;
}
parsoidSelector.each(parsoidSetupFieldsForTemplate);
}
if(selector.length) {
mw.loader.load('mediawiki.api');
mw.loader.load('mediawiki.ui.button');
mw.loader.load('mediawiki.ui.input');
$.ajax({
url: '/api/rest_v1/page/html/' + encodeURIComponent(mw.config.get('wgPageName')) + '/' + mw.config.get('wgRevisionId'),
headers: {
Accept: 'text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.5.0"',
'Api-User-Agent': 'editProtectedHelper (https://en.wikipedia.org/wiki/User:Jackmcbarn/editProtectedHelper)'
},
success: onParsoidDomReceived
});
}
});
});
}
// </nowiki>

/* TheTVExpert/submitRMTR.js */
//submitRMTR
//<nowiki>
$(function() {
function submitRMTR() {
var oldTitle = $('input[name=wpOldTitle]').val();
var newNamespace = mw.config.get('wgFormattedNamespaces')[$('select[name=wpNewTitleNs]').val()];
var newTitle = $('input[name=wpNewTitleMain]').val();
var newTitleFull = (newNamespace === '' ? newTitle : newNamespace + ':' + newTitle); 
var reason = $('input[name=wpReason]').val();
var rmtrText = '{{subst:RMassist|1=' + oldTitle + '|2=' + newTitleFull + '|reason=' + reason + '}}';
var textToFind = /---- and enter on a new line.* -->/;
var result;
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
titles: 'Wikipedia:Requested moves/Technical requests'
};
api.get(params).done(function(data) {
var page;
for (page in data.query.pages){
result = data.query.pages[page].revisions[0]['*'];
var newResult = result.replace(textToFind, '$&\n' + rmtrText);
var params2 = {
action: 'edit',
title: 'Wikipedia:Requested moves/Technical requests',
text: newResult,
summary: "Add request using [[User:TheTVExpert/submitRMTR|submitRMTR]]"
},
api2 = new mw.Api();
api2.postWithToken('csrf',params2).done(function(data){
console.log(data);
alert("Success.");
window.location = mw.util.getUrl('Wikipedia:Requested moves/Technical requests');
});
}
});
}
if (window.location.href.match('Special:MovePage')) {
var $rmtrButton = new OO.ui.ButtonWidget({
label:'Submit Technical Request',
flags: ['primary','progressive']
}).$element
.on('click',submitRMTR)
.appendTo($('button[name=wpMove]').parent().parent())
;
}
});
//</nowiki>

/* Andy_M._Wang/closeRM.js */
// <syntaxhighlight lang="javascript">
// closeRM by [[User:Andy M. Wang]]
// 1.0.2016.1008b
// based on [[User:Armbrust/closemrv.js]]
$(document).ready(function() {
"use strict";
// Add only when editing section
// Assumes Requested move has a level 2 header, ends at end of section
if ((document.title.indexOf("Editing ") === -1)
|| (document.title.indexOf("(section)") === -1)) {
return;
}
function rmClick(e) {
e.preventDefault();
var closeStr = prompt("Closing statement for requested move:");
if (closeStr === null || closeStr === '') {
alert("Invalid closing statement. Aborted.");
return;
}
var editform = document.editform, t = editform.wpTextbox1;
var lines = t.value.split('\n');
var inReqMoveTemplate = false;
var foundReqMoveTemplate = false;
var acc = '';
for (var idx in lines) {
if (lines[idx].toLowerCase().includes("{{requested move/dated")) {
acc += '{{subst:RM top|' + closeStr + "}}";
inReqMoveTemplate = !lines[idx].includes("}}");
foundReqMoveTemplate = true;
} else if (foundReqMoveTemplate && inReqMoveTemplate) {
inReqMoveTemplate = !lines[idx].includes("}}");
} else {
acc += lines[idx] + '\n';
}
}
if (!foundReqMoveTemplate) {
alert("{{requested move/dated}} NOT FOUND. Wikitext not updated\n\n"
+ "Possible reasons:\n"
+ "1. This edited section is not a requested move discussion.\n"
+ "2. The discussion was answered by another editor earlier.");
return;
}
t.value = acc + "{{subst:" + "RM bottom" + "}}";
editform.wpSummary.value += "Closing RM: " + closeStr.replace(
/\{\{subst\:rmnac\}\}|\'\'\'/gi,'').substring(0, 140)
+ (closeStr.length > 140 ? ' ...' : '')
+ ' ([[User:Andy M. Wang/closeRM|closeRM]])';
}
mw.loader.using( 'mediawiki.util' ).then( function(){
var portletLink = mw.util.addPortletLink("p-cactions", "#", "RM close",
"ca-rmclose", "Close a requested move discussion section");
$( portletLink ).click( rmClick );
} );
});
// </syntaxhighlight>

/* Suffusion_of_Yellow/effp-helper.js */
/* 
 * WP:EF/FP helper
 *
 * Copies text from Special:AbuseLog pages, and merges with later revisions if possible.
 *
 */
// <nowiki>
if (mw.config.get('wgAbuseFilterVariables')) {
if (window.effpUseDev === true)
mw.loader.load("https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/effp-helper-dev.js&action=raw&ctype=text/javascript");
else
mw.loader.load("https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/effp-helper-core.js&action=raw&ctype=text/javascript");
}
// </nowiki>

/* DannyS712/EFFPRH.js */
//<nowiki>
//Largely based on [[User:Enterprisey/AFCFFU.js]]
//Copied from [[User:Abelmoschus Esculentus/EFFPRH.js]]
//Includes contributions from [[User:EpicPupper]]
var EFFPRH_config = {
name: '[[User:DannyS712/EFFPRH|EFFPRH]]',
version: "1.3.1",
debug: false
};
var effp_ending = ' (' + EFFPRH_config.name + ' v.' + EFFPRH_config.version + ')';
var effp_effpPageName = mw.config.get('wgPageName').replace(/_/g, ' ');
var effp_effpSubmissions = new Array();
var effp_effpSections = new Array();
var effp_numTotal = 0;
var effp_AJAXnumber = 0;
var effp_Submissions = new Array();
var wgArticlePath = mw.config.get( 'wgArticlePath' );
function effp_editPage(title, newtext, summary, createonly, nopatrol) {
var edittoken = mw.user.tokens.get('csrfToken');
summary += effp_ending;
$("#effp_finished_wrapper").html('<span id="effp_AJAX_finished_' + effp_AJAXnumber + '" style="display:none">' + $("#effp_finished_wrapper").html() + '</span>');
var func_id = effp_AJAXnumber;
effp_AJAXnumber++;
$('#effp_status').html($('#effp_status').html() + '<li id="effp_edit' + jqEsc(title) + '">Editing <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></li>');
var request = {
'action': 'edit',
'title': title,
'text': newtext,
'summary': summary,
'token': edittoken
};
if (createonly) request.createonly = true;
var api = new mw.Api();
api.post(request)
.done(function ( data ) {
if ( data && data.edit && data.edit.result && data.edit.result == 'Success' ) {
$('#effp_edit' + jqEsc(title)).html('Saved <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a>');
} else {
$('#effp_edit' + jqEsc(title)).html('<span class="effp_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + JSON.stringify(data));
window.console && console.error('Edit failed on %s (%s). Error info: %s', wgArticlePath.replace("$1", encodeURI(title)), title, JSON.stringify(data));
}
} )
.fail( function ( error ) {
if (createonly && error == "articleexists")
$('#effp_edit' + jqEsc(title)).html('<span class="effp_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: The article already exists!');
else
$('#effp_edit' + jqEsc(title)).html('<span class="effp_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + error); 
})
.always( function () {
$("#effp_AJAX_finished_" + func_id).css("display", '');
});
if (!nopatrol) {
if ($('.patrollink').length) {
var patrolhref = $('.patrollink a').attr('href');
var rcid = mw.util.getParamValue('rcid', patrolhref);
if (rcid) {
$('#effp_status').html($('#effp_status').html() + '<li id="effp_patrol' + jqEsc(title) + '">Marking <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + ' as patrolled</a></li>');
var patrolrequest = {
'action': 'patrol',
'format': 'json',
'token': mw.user.tokens.get('patrolToken'),
'rcid': rcid
};
api.post(patrolrequest)
.done(function ( data ) {
if ( data ) {
$('#effp_patrol' + jqEsc(title)).html('Marked <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a> as patrolled');
} else {
$('#effp_patrol' + jqEsc(title)).html('<span class="effp_notice"><b>Patrolling failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span> with an unknown error');
window.console && console.error('Patrolling failed on %s (%s) with an unknown error.', wgArticlePath.replace("$1", encodeURI(title)), title);
}
} )
.fail( function ( error ) {
$('#effp_patrol' + jqEsc(title)).html('<span class="effp_notice"><b>Patrolling failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + error); 
});
}
}
}
}
function effp_escapeHtmlChars(original) {
return original.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function jqEsc(expression) {
return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~ ]/g, ''); 
}
function effp_generateSelect(title, options, onchange) {
var text = '<select name="' + title + '" id="' + title + '" ';
if (onchange !== null) text += 'onchange = "' + onchange + '" ';
text += '>';
for (var i = 0; i < options.length; i++) {
var o = options[i];
text += '<option value="' + effp_escapeHtmlChars(o.value) + '" ';
if (o.selected) text += 'selected="selected" ';
if (o.disabled) text += 'disabled ';
text += '>' + o.label + '</option>';
}
text += "</select>";
return text;
}
function effp_getPageText(title, show, redirectcheck, timestamp) {
if (show) $('#effp_status').html($('#effp_status').html() + '<li id="effp_get' + jqEsc(title) + '">Getting <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></li>');
var request = {
'action': 'query',
'prop': 'revisions',
'rvprop': 'content',
'format': 'json',
'indexpageids': true,
'titles' : title
};
if (redirectcheck) request.redirects = true;
if (timestamp) request.rvprop = 'content|timestamp';
var response = JSON.parse(
$.ajax({
url: mw.util.wikiScript('api'),
data: request,
async: false
})
.responseText
);
pageid = response['query']['pageids'][0];
if (pageid === "-1") {
if (show) $('#effp_get' +jqEsc(title)).html('The page <a class="new" href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a> does not exist');
return '';
}
var newtext = response['query']['pages'][pageid]['revisions'][0]['*'];
if (redirectcheck && response['query']['redirects'] /* If &redirects if specified but there is no redirect, this stops us from getting an error */){
var oldusername = response['query']['redirects'][0]['from'];
var newusername = response['query']['redirects'][0]['to'];
if ((typeof(oldusername) !== 'undefined') && (typeof(newusername) !== 'undefined') && (oldusername != newusername)){
usertalkpage = newusername;
if (show) {
$('#effp_status').html($('#effp_status').html() + '<li id="effp_get' + jqEsc(title) + '">Got <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + newusername + '">' + newusername + '</a> (page was renamed from ' + oldusername + ')</li>');
}
} else {
redirectcheck = false;
}
} else {
redirectcheck = false;
}
if (show && !redirectcheck)$('#effp_status').html($('#effp_status').html() + '<li id="effp_get' + jqEsc(title) + '">Got <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></li>');
if (!timestamp) return newtext;
else return {'pagetext':newtext,'timestamp':response['query']['pages'][pageid]['revisions'][0]['timestamp']};
}
function effp_init() {
var pagetext = effp_getPageText(effp_effpPageName, false);
var section_re = /==[^=]*==/;
pagetext = pagetext.substring(pagetext.search(section_re));
section_re = /==[^=]*==/g;
var section_headers = pagetext.match(section_re);
for (var i = 0; i < section_headers.length; i++) {
var section_start = pagetext.indexOf(section_headers[i]);
var section_text = pagetext.substring(section_start);
if (i < section_headers.length - 1) {
var section_end = section_text.substring(section_headers[i].length).indexOf(section_headers[i + 1]) + section_headers[i].length;
section_text = section_text.substring(0, section_end);
}
effp_effpSections.push(section_text);
}
for (var i = 0; i < effp_effpSections.length; i++) {
var header = effp_effpSections[i].match(section_re)[0];
header = header.slice(2, (header.length - 2));
var submission = {
type: 'effp',
from: new Array(),
section: i,
blockeduser: '',
admin: '',
cmt: '',
filteruser: '',
user: '',
title: header,
action: 'none',
blockuser : false,
expiry : '',
blockreason: '',
blockacc: false,
blockemail: false,
blocktalk: false,
blockauto: false,
anononly: false,
watch: false,
comment: ''
};
effp_effpSubmissions.push(submission);
effp_numTotal++;
}
for (var k = 0; k < effp_effpSubmissions.length; k++) {
var text = '<ul>';
text += '<li>Response: ';
selectoptions = [
{ label: 'None', selected: true, value: 'none' },
{ label: 'Done (no change to filter)', value: 'done' },
{ label: 'Done (may need a change to filter)', value: 'defm' },
{ label: 'Not Done (filter working properly)', value: 'notdone' },
{ label: 'Not Done (may need a change to filter)', value: 'ndefm' },
{ label: 'Not Done (notable people)', value: 'r' },
{ label: 'Already Done', value: 'alreadydone' },
{ label: 'Decline (edits are vandalism)', value: 'denied' },
{ label: 'Checking', value: 'checking' },
{ label: 'User blocked', value: 'blocked' },
{ label: 'Request on article talk page', value: 'talk' },
{ label: 'Fixed filter', value: 'fixed' },
{ label: 'Question', value: 'question' },
{ label: 'Note', value: 'note' },
{ label: 'Private filter', value: 'private' }
];
text += '<br/><label for="effp_effp_action' +  '">Action: </label>' + effp_generateSelect('effp_effp_action_'+k, selectoptions, 'effp_effp_onActionChange(' + k + ')') + '<div id="effp_effp_extra_' + k + '"></div></li>';
text += '</ul></li>';
text += '</ul>';
text += '<input type="button" id="effp_effp_done_button" name="effp_effp_done_button" value="Done" onclick="effp_effp_performActions(' + k + ')" />';
displayMessage_inline(text, 'effp-review-' + effp_effpSubmissions[k].section);
}
}
function effp_effp_performActions(sectionNumber) {
for (var i = 0; i < effp_effpSubmissions.length; i++) {
var action = $("#effp_effp_action_" + i).val();
effp_effpSubmissions[i].action = action;
if (action == 'none') continue;
if (action == 'blocked') {
effp_effpSubmissions[i].blockeduser = $.trim($("#effp_effp_blockeduser_" + i).val());
effp_effpSubmissions[i].admin = $.trim($("#effp_effp_admin_" + i).val());
effp_effpSubmissions[i].cmt = $.trim($("#effp_effp_cmt_" + i).val());
if (document.getElementById('effp_effp_blockuser_'+i).checked) {
effp_effpSubmissions[i].blockuser = true;
if ($('#effp_effp_blockreason_'+i).val() == 'custom') {
effp_effpSubmissions[i].blockreason = $.trim($('#effp_effp_customblockreason_'+i).val());
}
else effp_effpSubmissions[i].blockreason = $.trim($('#effp_effp_blockreason_'+i).val());
effp_effpSubmissions[i].expiry = $.trim($('#effp_effp_blockexpiry_'+i).val());
effp_effpSubmissions[i].blockacc = document.getElementById('effp_effp_blockacc_'+i).checked;
effp_effpSubmissions[i].blockemail = document.getElementById('effp_effp_blockemail_'+i).checked;
effp_effpSubmissions[i].blocktalk = document.getElementById('effp_effp_blocktalk_'+i).checked;
effp_effpSubmissions[i].anononly = document.getElementById('effp_effp_anononly_'+i).checked;
effp_effpSubmissions[i].watch = document.getElementById('effp_effp_watch_'+i).checked;
}
}
if (action == 'fixed') {
effp_effpSubmissions[i].filteruser = $.trim($("#effp_effp_filteruser_" + i).val());
effp_effpSubmissions[i].cmt = $.trim($("#effp_effp_cmt_" + i).val());
}
if (action == 'alreadydone') {
effp_effpSubmissions[i].user = $.trim($("#effp_effp_user_" + i).val());
effp_effpSubmissions[i].cmt = $.trim($("#effp_effp_cmt_" + i).val());
}
if (action == 'talk') {
effp_effpSubmissions[i].talk = $.trim($("#effp_effp_talk_" + i).val());
effp_effpSubmissions[i].cmt = $.trim($("#effp_effp_cmt_" + i).val());
}
if (action != 'none' && action != 'blocked' && action != 'fixed' && action != 'alreadydone') {
effp_effpSubmissions[i].cmt = $.trim($("#effp_effp_cmt_" + i).val());
}
}
displayMessage_inline('<ul><li><b>Now processing...</li></ul><ul id="effp_status"></ul><ul id="effp_finish"></ul>', 'effp-review-' + sectionNumber);
$('#effp_finish').html('<span id="effp_finished_main" style="display:none"><li id="effp_done"><b>Done (<a href="' + wgArticlePath.replace("$1", encodeURI(effp_effpPageName)) + '?action=purge" title="' + effp_effpPageName + '">Reload page</a>)</b></li></span>');
pagetext = effp_getPageText(effp_effpPageName, true);
var effp_total = 0;
for (var i = 0; i < effp_effpSubmissions.length; i++) {
var sub = effp_effpSubmissions[i];
if (pagetext.indexOf(effp_effpSections[sub.section]) == -1) {
var $error = $('<li>').text( 'Skipping ' + sub.title + ': Cannot find section. Perhaps it was modified in the mean time?');
$('#effp_status').append( $error );
continue;
}
var origtext = effp_effpSections[sub.section];
var text = effp_effpSections[sub.section];
var startindex = pagetext.indexOf(effp_effpSections[sub.section]);
var endindex = startindex + text.length;
if (text === origtext) {
var sub_m = effp_effpSubmissions[i];
if (sub_m.blockuser == true) {
var match = /\[\[(?:User[_ ]talk:|User:|Special:Contributions\/)([^\||\]\]]*)([^\]]*?)\]\]/i.exec(text);
if (match) {
var vandal = match[1];
var API = new mw.Api();
API.postWithToken("block", {
'action': 'block',
'expiry': sub_m.expiry,
'allowusertalk': !sub_m.blocktalk,
'noemail': sub_m.blockemail,
'anononly': sub_m.anononly,
'autoblock': sub_m.blockauto,
'nocreate': sub_m.blockacc,
'reason': sub_m.blockreason,
'watchuser': sub_m.watch,
'user': vandal
}).done(function(blockData) {
$('#effp_status').html($('#effp_status').html()+'<li>Blocked '+vandal+' succesfully</li>');
effp_effp_templateUser(sub_m.expiry, vandal);
}).fail(function(error) {
$("#effp_status").html(
$('#effp_status').html()+"<li><b>Error</b> blocking <a href='"+mw.util.getUrl('User:'+vandal)+"'>"+vandal+"</a>: "+error+"</li>"
);
effp_effp_templateUser(sub_m.expiry, vandal);
});
}
}
if (sub_m.action == 'blocked') {
text += '\n*\{\{effp|blocked|'+sub_m.blockeduser+'|'+sub_m.admin+'\}\} '+sub_m.cmt+' \~\~\~\~\n';
effp_total++;
}
if (sub_m.action == 'fixed') {
text += '\n*\{\{effp|fixed|'+sub_m.filteruser+'\}\} '+sub_m.cmt+' \~\~\~\~\n';
effp_total++;
}
if (sub_m.action == 'alreadydone') {
text += '\n*\{\{effp|alreadydone|'+sub_m.user+'\}\} '+sub_m.cmt+' \~\~\~\~\n';
effp_total++;
}
if (sub_m.action == 'talk') {
text += '\n*\{\{effp|talk|'+sub_m.talk+'\}\} '+sub_m.cmt+' \~\~\~\~\n';
effp_total++;
}
if (sub_m.action == undefined) {
// Something went wrong...
continue;
}
if (sub_m.action != 'none' && sub_m.action != 'blocked' && sub_m.action != 'talk' && sub_m.action != 'fixed' && sub_m.action != 'alreadydone') {
text += '\n*\{\{effp|'+sub_m.action+'\}\} '+sub_m.cmt+' \~\~\~\~\n';
effp_total++;
}
pagetext = pagetext.substring(0, startindex) + text + pagetext.substring(endindex);
}
}
var summary;
if (effp_total==1) summary = "Responding to "+effp_total+" report";
else summary = "Responding to "+effp_total+" reports";
pagetext = pagetext.replace(/[\n\r]{3,}/g,"\n\n");
pagetext = pagetext.replace(/[\n\r]+==/g,"\n\n==");
effp_editPage(effp_effpPageName, pagetext, summary, false);
$(document).ajaxStop(function () {
$("#effp_finished_main").css("display", "");
});
}
function effp_effp_templateUser(duration, vandal) {
var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
d = new Date();
var ApI = new mw.Api();
ApI.postWithToken( "edit", {
action: "edit",
section: 'new',
watchlist: "nochange",
sectiontitle: monthNames[d.getMonth()] + ' ' + d.getFullYear(),
summary: "You have been blocked from editing for abuse of editing privileges."+effp_ending,
text: "\n{{subst:uw-block|time="+duration+"|sig=yes}}",
title: "User talk:"+vandal
}).done(function(editData) {
$('#effp_status').html($('#effp_status').html()+'<li>Successfully posted block notice on <a href="' + wgArticlePath.replace("$1", encodeURI('User talk:'+vandal)) + '">User talk:'+vandal+'</a></li>');
}).fail(function(error) {
$('#effp_status').html($('#effp_status').html()+'<li>Error posting block notice on <a href="' + wgArticlePath.replace("$1", encodeURI('User talk:'+vandal)) + '">User talk:'+vandal+'</a></li>');
});
}
function effp_effp_onActionChange(id) {
var extra = $("#effp_effp_extra_" + id);
var selectValue = $("#effp_effp_action_" + id).val();
if (selectValue == 'none') extra.html('');
if (selectValue == 'blocked' && !Morebits.userIsInGroup('sysop')) {
extra.html('<label for="effp_effp_blockeduser_'+id+'">Which user is blocked? </label><input type="text" '+'name="effp_effp_blockeduser_'+id+'" id="effp_effp_blockeduser_'+id+'"><br/><label for="effp_effp_admin_'+id+'">Who blocked the user? </label><input type="text" '+'name="effp_effp_admin_'+id+'" id="effp_effp_admin_'+id+'"><br/><label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'">');
}
if (selectValue == 'fixed') {
extra.html('<label for="effp_effp_filteruser_'+id+'">Who fixed the filter? </label><input type="text" '+'name="effp_effp_filteruser_'+id+'" id="effp_effp_filteruser_'+id+'"><br/><label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'">');
}
if (selectValue == 'alreadydone') {
extra.html('<label for="effp_effp_user_'+id+'">Who made the edit? </label><input type="text" '+'name="effp_effp_user_'+id+'" id="effp_effp_user_'+id+'"><br/><label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'">');
}
if (selectValue == 'talk') {
extra.html('<label for="effp_effp_talk_'+id+'">Please enter the article name: </label><input type="text" '+'name="effp_effp_talk_'+id+'" id="effp_effp_talk_'+id+'"><br/><label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'">');
}
if (selectValue != 'none' && selectValue != 'blocked' && selectValue != 'fixed' && selectValue != 'talk' && selectValue != 'alreadydone') {
extra.html('<label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'">');
}
if (Morebits.userIsInGroup('sysop')) {
if (selectValue == 'blocked') {
extra.html('<label for="effp_effp_blockeduser_'+id+'">Which user is blocked? </label><input type="text" '+'name="effp_effp_blockeduser_'+id+'" id="effp_effp_blockeduser_'+id+'"><br/><label for="effp_effp_admin_'+id+'">Who blocked the user? </label><input type="text" '+'name="effp_effp_admin_'+id+'" id="effp_effp_admin_'+id+'"><br/><label for="effp_effp_cmt_'+id+'">Comment: </label><input type="text" '+'name="effp_effp_cmt_'+id+'" id="effp_effp_cmt_'+id+'"><br/><input type="checkbox" '+'name="effp_effp_blockuser_'+id+'" id="effp_effp_blockuser_'+id+'" onclick="effp_effp_onActionChange2('+id+');"/><label for="effp_effp_blockuser_'+id+'">Block user</label><br/><div id="effp_effp_blockuserdiv_'+id+'"></div>');
}
}
}
function effp_effp_onActionChange2(id) {
var blockuserdiv = $("#effp_effp_blockuserdiv_"+id);
if (!document.getElementById('effp_effp_blockuser_'+id).checked) {
blockuserdiv.html('');
}
else {
var blockreasons = [
{ label: 'None', selected: true, value: '' },
{ label: 'Vandalism', value: '[[WP:V|Vandalism]]' },
{ label: 'Deliberately triggering the edit filter', value: 'Deliberately triggering the [[WP:FILTER|edit filter]]' },
{ label: 'Disruptive editing', value: '[[WP:DE|Disruptive editing]]' },
{ label: 'Clearly not here to contribute to the encyclopedia', value: 'Clearly [[WP:NOTHERE|not here to contribute to the encyclopedia]]' },
{ label: 'Custom', value: 'custom' }
];
var blockexpiration = [
{ label: '31 hours', selected: true, value: '31 hours' },
{ label: 'indefinite', value: 'indefinite' },
{ label: '24 hours', value: '24 hours' },
{ label: '48 hours', value: '48 hours' },
{ label: '72 hours', value: '72 hours' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '3 weeks', value: '3 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '6 months', value: '6 months' },
{ label: '1 year', value: '1 year' },
{ label: '2 years', value: '2 years' }
];
blockuserdiv.html('<label for="effp_effp_blockreason_'+id+'">Block reason: </label>'+effp_generateSelect('effp_effp_blockreason_'+id, blockreasons)+'<label for="effp_effp_customblockreason_'+id+'"> Custom block reason (if "custom" is selected): </label><input type="text" id="effp_effp_customblockreason_'+id+'" name="effp_effp_customblockreason_'+id+'"><br/><label for="effp_effp_customblockreason_'+id+'">Expiration: </label>'+effp_generateSelect('effp_effp_blockexpiry_'+id, blockexpiration)+'<br/><input type="checkbox" '+'name="effp_effp_blockacc_'+id+'" id="effp_effp_blockacc_'+id+'"><label for="effp_effp_blockacc_'+id+'">Prevent account creation</label><br/><input type="checkbox" '+'name="effp_effp_blockemail_'+id+'" id="effp_effp_blockemail_'+id+'"><label for="effp_effp_blockemail_'+id+'">Prevent user from sending Email</label><br/><input type="checkbox" '+'name="effp_effp_blocktalk_'+id+'" id="effp_effp_blocktalk_'+id+'"><label for="effp_effp_blocktalk_'+id+'">Block user from editing their talk page</label><br/><input type="checkbox" '+'name="effp_effp_blockauto_'+id+'" id="effp_effp_blockauto_'+id+'"><label for="effp_effp_blockauto_'+id+'">Automatically block the last IP address used by this user, and any subsequent IP addresses they try to edit from</label><br/><input type="checkbox" '+'name="effp_effp_anononly_'+id+'" id="effp_effp_anononly_'+id+'"><label for="effp_effp_anononly_'+id+'">Prevent logged-in users from editing from this IP address</label><br/><input type="checkbox" '+'name="effp_effp_watch_'+id+'" id="effp_effp_watch_'+id+'"><label for="effp_effp_watch_'+id+'">Watch this user'+"'"+'s user and talk pages</label>');
}
}
function displayMessage_inline(message, div, className) {
var divtitle = '#' + div;
if (message === '' || message === null) {
$(divtitle).empty().hide();
return true;
} else {
var $messageDiv = $(divtitle);
$messageDiv.attr('style', "margin:1em;padding:0.5em 2.5%;border:solid 1px #ddd;background-color:#fcfcfc");
if (!$messageDiv.length) {
if (mw.util.$content.length) {
mw.util.$content.prepend($messageDiv);
} else {
return false;
}
}
if (typeof message === 'object') {
$messageDiv.empty();
$messageDiv.append(message);
} else {
$messageDiv.html(message);
}
$messageDiv.slideDown();
return true;
}
}
function effp_links() {
var sectionHeaders = $("#mw-content-text h2");
var offset = 1;
sectionHeaders.each(function(index, element) {
var not_archived = !$(element).next().length || $(element).next().html().indexOf('This is an archived discussion.') == -1;
if (index > 0)
var idtitle = "effp-review-" + (index - 1);
$('<div id="' + idtitle + '" style="display:none;"></div>').insertAfter(element);
var editSectionLink = $(element).children(".mw-editsection");
if ((editSectionLink.length > 0) && (not_archived)) {
editSectionLink = editSectionLink[0];
var reviewlink = document.createElement("a");
reviewlink.href = "#" + idtitle;
$(reviewlink).attr("sectionIndex", index + offset);
reviewlink.innerHTML = "Review report";
var editSectionContents = $(editSectionLink).html();
editSectionLink.innerHTML = "[";
editSectionLink.appendChild(reviewlink);
editSectionLink.innerHTML = editSectionLink.innerHTML + "] " + editSectionContents;
reviewlink.onclick = (function() {
$(reviewlink).remove();
effp_init();
});
} else {
offset = offset - 1;
}
});
$('body [sectionIndex]').click((function() {
$('body [sectionIndex]').each(function(i) {
$(this).html("Reviewing reports...").contents().unwrap();
});
effp_init();
}));
}
mw.loader.using(['ext.gadget.Twinkle', 'mediawiki.util', 'mediawiki.api', 'mediawiki.Title'], function() {
$(document).ready( function () { 
setTimeout(function (){
if ( mw.config.get( "wgPageName" ) === "Wikipedia:Edit_filter/False_positives/Reports" || mw.config.get( "wgPageName" ) === "User:DannyS712/EFFPRH/sandbox" ) {
effp_links();
}
}, 1000);
} );
});
//</nowiki>

/* Wugapodes/Capricorn.js */
// <nowiki>
// This is a modified version of [[User:Sam Sailor/Scripts/Sagittarius+.js]] ([[Special:PermaLink/899463476]])
// Docs: [[User:Wugapodes/Capricorn]]
/*jshint undef:true, latedef:true, shadow:true, loopfunc:true, scripturl:true, undef:true */
/*globals jQuery, mw, importStylesheet */
////////////////////////////////////////////////////////////////////////////////
// Helper function definitions
function encodeCodePoint(c) {
if (c === 0x20)
return '_';
if (c < 0x80) {
return '.' + c.toString(16).toUpperCase();
} else if (c < 0x800) {
return '.' + (0xc0 |  (c >>>  6)).toString(16).toUpperCase() +
'.' + (0x80 | ( c & 0x3f)).toString(16).toUpperCase();
} else if (c < 0x10000) {
return '.' + (0xe0 |  (c >>> 12)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>>  6) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ( c & 0x3f)).toString(16).toUpperCase();
} else if (c < 0x200000) {
return '.' + (0xf0 |  (c >>> 18)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 12) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>>  6) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ( c & 0x3f)).toString(16).toUpperCase();
} else if (c < 0x4000000) {
return '.' + (0xf8 |  (c >>> 24)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 18) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 12) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>>  6) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ( c & 0x3f)).toString(16).toUpperCase();
} else if (c < 0x80000000) {
return '.' + (0xfc |  (c >>> 30)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 24) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 18) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>> 12) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ((c >>>  6) & 0x3f)).toString(16).toUpperCase() +
'.' + (0x80 | ( c & 0x3f)).toString(16).toUpperCase();
}
}
function normaliseAnchor(anchor) {
// "." is not escaped!
return anchor.replace(/[^0-9A-Za-z_:\.]/g, function (m) { /* [\ud800-\udbff][\udc00-\dfff]| */
if (m.length === 2) { // surrogate pair
return encodeCodePoint((m.charCodeAt(0) & 0x3ff) << 10 | m.charCodeAt(1) & 0x3ff);
} else {
return encodeCodePoint(m.charCodeAt(0));
}
});
}
function normaliseTitle(title) {
try {
var t = new mw.Title(title);
return t.getPrefixedText();
} catch (e) {
return null;
}
}
function el(tag, child, attr, events) {
var node = document.createElement(tag);
 
if (child) {
if ((typeof child === 'string') || (typeof child.length !== 'number'))
child = [child];
for (var i = 0; i < child.length; ++i) {
var ch = child[i];
if ((ch === void(null)) || (ch === null))
continue;
else if (typeof ch !== 'object')
ch = document.createTextNode(String(ch));
node.appendChild(ch);
}
}
if (attr) for (var key in attr) {
if ((attr[key] === void(0)) || (attr[key] === null))
continue;
node.setAttribute(key, String(attr[key]));
}
if (events) for (var key in events) {
var handler = events[key];
if ((key === 'input') && (window.oninput === void(0))) {
key = 'change';
}
node.addEventListener(key, handler, false);
}
return node;
}
function link(child, href, attr, ev) {
attr = attr || {};
ev = ev || {};
if (typeof attr === 'string') {
attr = { "title": attr };
}
if (typeof href === 'string')
attr.href = href;
else {
attr.href = 'javascript:void(null);';
ev.click = href;
}
return el('a', child, attr, ev);
}
var templateGroups = {
"fromRelatedInfo": "Related information, From",
"toRelatedInfo": "Related information, To",
"fromPartOfSpeech": "Parts of speech, From",
"fromEngVar": "English variant spelling, From",
"fromOrthographicModification": "Orthographic difference, From",
"toOrthographicModification": "Orthographic difference, To",
"fromAlt": "Alternative names, From",
"fromDisambiguation": "Ambiguity, From",
//"toDisambiguation": "Ambiguity, To",
//"fromSpecificity": "Specificity, From",
"fromAnthroponym": "Anthroponym, From",
"fromFiction": "Fiction, From",
"fromWork": "Works of art and works generally, From",
"toWork": "Works of art and works generally, To",
"fromLocationOrInfrastructure": "Geographic location or infrastructure, From",
"fromFormerName": "Former names, From",
"toFormerName": "Former names, To",
"fromSystematicName": "Systematic name, From",
"toSystematicName": "Systematic name, To",
"fromPostal": "From postal information",
"fromOrganization": "From organization",
"fromMath": "From mathematical topic",
"fromComic": "Comics, From",
"toComic": "Comics, To",
"fromMiddleEarth": "Middle-earth topic, From",
"toMiddleEarth": "Middle-earth topic, To",
"fromMisc": "From miscellaneous information",
"fromMeta": "Meta information, From",
"toMeta": "Meta information, To",
"fromProtected": "Protection level, From",
"toNameSpace": "Namespaces, To",
"fromPrintworthiness":"Printworthiness"
};
////////////////////////////////////////////////////////////////////////////////
// Callback functions
function mainCallback(aliasJSON, templateJSON) {
var templateAliases = aliasJSON;
var redirectTemplates = templateJSON;
//console.log(templateAliases);
//console.log(redirectTemplates);
// <nowiki>
'use strict';
importStylesheet('User:Wugapodes/Capricorn.css');

var wgNamespaceIds = mw.config.get('wgNamespaceIds');
var contentText = document.getElementById('mw-content-text');
var firstHeading = document.getElementById('firstHeading');
var redirMsg = contentText.getElementsByClassName('redirectMsg')[0];
var uiWrapper = el('div');
var edittoken = null;

function MarkupBlob(markup) {
if (!markup) {
this.target = '';
this.rcatt = {};
this.tail = '';
} else
this.parse(markup);
}
MarkupBlob.prototype.parse = function (markup) {
var rdrx = /^#REDIRECT:?\s*\[\[\s*([^\|{}[\]]+?)\s*]]\s*/i;
var tprx = /^\s*{{([A-Za-z ]+)((?:\|(?:[^|{}]*|{{[^|}]*}})+)*)}}\s*/i;
var m;
m = rdrx.exec(markup.trim())
markup = markup.substr(m[0].length);
this.target = m[1];
this.rcatt = {};
out: while ((m = tprx.exec(markup))) {
var alias = normaliseTitle(m[1]);
while (templateAliases[alias])
alias = templateAliases[alias]; // hopefully there are no loops.
if (alias === "This is a redirect") {
var params = m[2].split('|');
for (var j = 0; j < params.length; ++j) {
if (!params[j])
continue;
if (params[j].indexOf('=') !== -1)
break out;
alias = normaliseTitle("R " + params[j]);
while (templateAliases[alias])
alias = templateAliases[alias]; // hopefully there are still no loops.
if (alias in redirectTemplates)
this.rcatt[alias] = true;
else
break out;
}
} else if (alias === "Redirect category shell") {
var mm, rr = /{{(.*?)}}/g;
while (mm = rr.exec(m[2])) {
alias = normaliseTitle(mm[1]);
while (templateAliases[alias])
alias = templateAliases[alias];
if (alias in redirectTemplates)
this.rcatt[alias] = true;
}
} else if (alias in redirectTemplates) {
if (m[2]) // TODO
break;
this.rcatt[alias] = true;
} else {
break;
}
markup = markup.substr(m[0].length);
}
this.tail = markup;
};
MarkupBlob.prototype.toString = function () {
var markup = '#REDIRECT [[' + this.target + ']]\n';
var tail = '';
var wrapped = [];
for (var key in this.rcatt) {
if (this.rcatt[key])
if ((wrapped.length < 6) && /^R\s+/.test(key))
wrapped.push('{{' + key + '}}\n');
else
tail += '{{' + key + '}}\n';
}
if (wrapped.length)
markup += "\n{{Redirect category shell|\n" + wrapped.join("") + "}}\n";
markup += tail + '\n';
markup += this.tail;
return markup;
};
function buildTagList(rcatt) {
function makeCheckBox(key) {
return el('label', [
el('input', null, {
type: "checkbox",
checked: (key in rcatt) ? "checked" : null,
}, {
change: function (ev) {
rcatt[key] = this.checked;
}
}),
' ',
redirectTemplates[key].label
], {
"title": redirectTemplates[key].tooltip
});
}
var list = el('dl', null, { "class": "tag-list" });
var group = {};
for (var key in templateGroups) {
list.appendChild(el('dt', templateGroups[key]));
list.appendChild(el('dd', group[key] = el('ul')));
}
for (var key in redirectTemplates) {
var label = makeCheckBox(key);
group[redirectTemplates[key].group].appendChild(el('li', label));
}
var collapsibleContent = el('div', list, { 
"class": "mw-collapsible-content",
"id": "capricorn-toggle-content"
})
return collapsibleContent;
}
//
//  Interface creation
//
function buildEditingUI(mblob, saveCallback) {
var statusbar;
var needsCheck = true;
var doSave;
var uiLink, uiTarget;
mblob = mblob || new MarkupBlob();
// Change text of status bar
function setStatus(status) {
while (statusbar.firstChild) // Remove previous statuses
statusbar.removeChild(statusbar.firstChild);
if (status) { // If status is a string, add it
if (typeof status === 'string')
statusbar.appendChild(document.createTextNode(status));
else {  // Otherwise, loop through list and add statuses
for (var j = 0; j < status.length; ++j) {
if (typeof status[j] === 'string')
statusbar.appendChild(document.createTextNode(status[j]));
else
statusbar.appendChild(status[j]);
}
}
}
}
// Check if the target has changed??
// Not actually sure what this does yet 21 Oct 2019
function inputChanged(ev) {
/*jshint validthis:true */
try {
mblob.target = this.value;
var t = new mw.Title(this.value);
var frag = t.getFragment() ? '#' + normaliseAnchor(t.getFragment()) : '';
if (uiLink) uiLink.href = mw.util.getUrl(t.getPrefixedDb(), { redirect: "no" }) + frag;
setStatus();
} catch (e) {
setStatus('Invalid title.');
if (uiLink) uiLink.href = 'javascript:void(0);';
}
needsCheck = true;
}
var uiStatusLine;
var patrolLine;
var origTarget = mblob.target
var ui = el('form', [
el('div', [
el('ul', [
el('li', [
uiTarget = el('input', null, {
'type': 'text',
'class': 'redirectText',
'value': mblob.target
}, {
'input': inputChanged,
'change': inputChanged,
'blur': function (ev) { // i would not have to write this, if it were not for jQuery. seriously.
if (mblob.target === this.value)
return;
inputChanged.call(this, ev);
}
})
])
], { 'class': 'redirectText' }),
el('input', null, {
"type": "button",
"class":"capricorn-toggle",
"id": "capricorn-toggle-button",
"value": "Hide rcat list"
}, {
'click': function() {
$( "#capricorn-toggle-content" ).toggle();
var buttonText = $("#capricorn-toggle-button")[0].value;
if (buttonText === "Hide rcat list") {
$("#capricorn-toggle-button")[0].value = "Show rcat list"
} else {
$("#capricorn-toggle-button")[0].value = "Hide rcat list"
};
}
})
], { 'class': 'redirectMsg' }),
buildTagList(mblob.rcatt),
uiStatusLine = el('p', [
patrolLine = el('span', [], {}),
statusbar = el('span', [], {
'class': 'status-line'
}),
el('span', [
link(["Statistics for this page"], 'https://tools.wmflabs.org/pageviews?project=en.wikipedia.org&pages=' + encodeURIComponent(mw.config.get('wgPageName'))),
' • ',
link(["WP:TMR"], mw.util.getUrl("Wikipedia:Template messages/Redirect pages")),
' • ',
link(["About Capricorn"], mw.util.getUrl("User:Wugapodes/Capricorn"))
], {
'style': 'float: right;'
})
])
], {
'action': 'javascript:void(0)',
'class': 'kephir-sagittarius-editor'
}, {
'submit': function (ev) {
ev.preventDefault();
if (uiStatusLine.childNodes[1].childNodes[0]) {
var patrolVal = uiStatusLine.childNodes[1].childNodes[0].childNodes[0].checked;
if (patrolVal) {
api.get({
"action": "query",
"format": "json",
"prop": "revisions",
"meta": "tokens",
"titles": mw.config.get('wgPageName'),
"rvprop": "ids",
"rvslots": "",
"rvlimit": "1",
"rvdir": "newer",
"type": "patrol"
}, {
success: function (result) {
//console.log(mw.config.get('wgPageName'))
var patrolToken = result["query"]["tokens"]["patroltoken"];
var revIDpart = result["query"]["pages"];
var revID = null;
for (var page in revIDpart) {
revID = revIDpart[page]["revisions"][0]["revid"];
}
//console.log(revID)
api.post({
"action": 'patrol',
"revid": revID,
"token": patrolToken
}, {
success: function (result) {
if (result.error) {
console.log(result.error);
setStatus([
'API error: "',
result.error.info,
'" [code: ', el('code', [result.error.code]), ']'
]);
console.log(result.error)
return;
}
}
});
}
});
}
}
ui.doCheck(saveCallback);
}
});
ui.statusLine = uiStatusLine;
ui.patrolLine = patrolLine;
ui.origTarget = origTarget
var sectCache = {};
var $uiTarget = jQuery(uiTarget);
$uiTarget.suggestions({
submitOnClick: false,
delay: 500,
fetch: function (query) {
$uiTarget.suggestions('suggestions', []);
if (query.indexOf('#') !== -1) {
var title = query.substr(0, query.indexOf('#'));
var sect = query.substr(query.indexOf('#') + 1);
if (sectCache[title]) {
var normSect = normaliseAnchor(sect);
$uiTarget.suggestions('suggestions',
sectCache[title].filter(function (item) {
var norm = normaliseAnchor(item.anchor);
return norm.substr(0, normSect.length) === normSect;
})
);
return;
}
api.get({
action: 'parse',
page: title,
prop: 'sections|properties',
redirects: '1'
}).then(function (result) {
if (result.parse.redirects && result.parse.redirects.length) {
// XXX
return;
}
var disambig = false; // XXX
var normSect = normaliseAnchor(sect);
sectCache[title] = result.parse.sections.map(function (item) {
return {
anchor: item.anchor,
title: title + '#' + decodeURIComponent(item.anchor.replace(/_/g, ' ').replace(/\.([0-9A-Fa-f][0-9A-Fa-f])/g, '%')), // XXX: hack
disambig: disambig,
toString: function () {
return this.title;
}
};
});
$uiTarget.suggestions('suggestions',
sectCache[title].filter(function (item) {
var norm = normaliseAnchor(item.anchor);
return norm.substr(0, normSect.length) === normSect;
})
);
});
return;
}
api.get({
action: 'query',
generator: 'allpages',
gapprefix: query,
gaplimit: 16,
prop: 'info|pageprops',
}).then(function (result) {
var pglist = [];
for (var pgid in result.query.pages) {
var page = result.query.pages[pgid];
pglist.push({
title: page.title,
pageid: page.pageid,
disambig: page.pageprops && ('disambiguation' in page.pageprops),
redirect: 'redirect' in page,
toString: function () {
return this.title;
}
});
}
$uiTarget.suggestions('suggestions', pglist);
});
},
result: {
render: function (item, content) {
var elm = this[0];
elm.appendChild(el('span', [item.title], {
style: item.redirect ? 'font-style: italic' : ''
}));
if (item.disambig)
elm.appendChild(el('small', [' (disambiguation page)']));
if (item.redirect)
elm.appendChild(el('small', [' (redirect)']));
},
select: function ($textbox) {
var item = this.data('text');
var textbox = $textbox[0];
textbox.value = item.title;
if (item.redirect) {
api.get({
action: 'query',
pageids: item.pageid,
redirects: '1'
}).then(function (result) {
var redir = result.query.redirects.pop();
textbox.value = redir.to + (redir.tofragment ? '#' + redir.tofragment : '');
});
}
return true;
}
}
});
ui.doCheck = function (callback) {
var that = this;
if (!/^\s*[^\|{}[\]]+\s*$/.test(mblob.target)) {
setStatus(['Error: the target page name is invalid.']);
return;
}
if (needsCheck) {
var oldTarget = mblob.target;
var normTarget;
try {
normTarget = new mw.Title(oldTarget);
} catch (e) {
setStatus(['"', oldTarget, '" is not a valid page name. Try again to proceed anyway.']);
return;
}
setStatus(['Checking target validity...']);
needsCheck = false;
api.get({
action: 'parse',
page: oldTarget = mblob.target,
prop: 'sections',
redirects: '1'
}, {
success: function (result) {
var m;
if (result.error) {
if (result.error.code === 'missingtitle') {
setStatus([
'Error: The target page "',
link([normTarget.getPrefixedText()], mw.util.getUrl(normTarget.getPrefixedText(), { "class": "new" })),
'" does not exist. Try again to proceed anyway.'
]);
} else {
setStatus([
'API error: "',
result.error.info,
'" [code: ', el('code', [result.error.code]), ']'
]);
}
return;
}
if (result.parse.redirects && result.parse.redirects[0]) {
var newTarget = result.parse.redirects[0].to + (result.parse.redirects[0].tofragment ? "#" + result.parse.redirects[0].tofragment : "");
setStatus([
'Error: The target page "',
link([normTarget.getPrefixedText()], mw.util.getUrl(normTarget.getPrefixedText(), { redirect: "no" })),
'" is already a redirect to "',
link([newTarget], mw.util.getUrl(newTarget, { redirect: "no" })),
'". Try again to proceed anyway, or ',
link(['retarget this redirect to point there directly'], function () {
uiTarget.value = mblob.target = newTarget +
((!result.parse.redirects[0].tofragment && normTarget.fragment) ? '#' + normTarget.fragment : '');
needsCheck = true;
}),
'.'
]);
return;
}
if (normTarget.fragment) { // we have a section link
var sect = normaliseAnchor(normTarget.fragment);
var isValidSect = false;
var sectlist = result.parse.sections;
for (var j = 0; j < sectlist.length; ++j) {
if (sectlist[j].anchor === sect)
isValidSect = true;
}
if (!isValidSect) {
setStatus([
'Error: The target page "',
link([normTarget.getPrefixedText()], mw.util.getUrl(normTarget.getPrefixedText(), { redirect: "no" })),
'" does not have a a section called "',
normTarget.fragment,
'". Try again to proceed anyway.'
]);
return;
}
}
callback(setStatus);
}
});
return;
}
callback(setStatus);
};
return ui;
}
function setSummary(current,orig) {
var summary;
if (orig === current) {
summary= "Modifying [[WP:RCAT|redirect categories]] using [[User:Wugapodes/Capricorn|Capricorn ♑]]";
} else {
summary = 'Redirecting to [[' + current + ']] ([[User:Wugapodes/Capricorn|♑]])'
}
return summary
}
if ((mw.config.get('wgAction') === 'view') && (mw.config.get('wgArticleId') === 0)) { // nonexistent page.
uiWrapper.appendChild(el('div', [
link(['Create a redirect'], function () {
while (uiWrapper.hasChildNodes())
uiWrapper.removeChild(uiWrapper.firstChild);
var mblob = new MarkupBlob();
var ui = buildEditingUI(mblob, function (setStatus) {
setStatus(['Saving...']);
var summary = setSummary(mblob.target,ui.origTarget)
api.post({
action: 'edit',
title: mw.config.get('wgPageName'),
createonly: 1,
summary: summary,
text: mblob.toString(),
token: mw.user.tokens.get('csrfToken')
}, {
success: function (result) {
if (result.error) {
setStatus([
'API error: "',
result.error.info,
'" [code: ', el('code', [result.error.code]), ']'
]);
return;
}
setStatus(['Saved. Reloading page...']);
if (/redirect=no/.test(location.href)) // XXX
location.reload();
else
location.search = location.search ? location.search + '&redirect=no' : '?redirect=no';
}
});
});
ui.statusLine.insertBefore(el('input', null, {
type: 'submit',
value: 'Save'
}), ui.statusLine.firstChild);
uiWrapper.appendChild(ui);
}), ' from this page with Capricorn'
], {
"class": "kephir-sagittarius-invite"
}));
contentText.parentNode.insertBefore(uiWrapper, contentText);
} else if ((mw.config.get('wgAction') === 'view') && mw.config.get('wgIsRedirect') && redirMsg) {
// start editor immediately
uiWrapper.appendChild(el('div', ['Loading page source…'], {
"class": "kephir-sagittarius-loading"
}));
contentText.insertBefore(uiWrapper, contentText.firstChild);
api.get({
action: 'query',
prop: 'revisions',
rvprop: 'timestamp|content',
pageids: mw.config.get('wgArticleId'),
rvstartid: mw.config.get('wgRevisionId'),
rvlimit: 1,
rvdir: 'older'
}, {
success: function (result) {
if (result.error) {
uiWrapper.appendChild(el('div', [
'API error: "',
result.error.info,
'" [code: ', el('code', [result.error.code]), ']. Reload to try again.'
], {
"class": "kephir-sagittarius-error"
}));
return;
}
while (uiWrapper.hasChildNodes())
uiWrapper.removeChild(uiWrapper.firstChild);
var page = result.query.pages[mw.config.get('wgArticleId')];
var mblob;
var token = mw.user.tokens.get('csrfToken')
try {
mblob = new MarkupBlob(page.revisions[0]['*']);
} catch(e) {
uiWrapper.appendChild(el('div', ['Error: unable to parse page. Edit the source manually.'], {
"class": "kephir-sagittarius-error"
}));
return;
}
redirMsg.parentNode.removeChild(redirMsg);
var ui = buildEditingUI(mblob, function (setStatus) {
setStatus(['Saving...']);
var summary = setSummary(mblob.target,ui.origTarget)
api.post({
action: 'edit',
title: mw.config.get('wgPageName'),
basetimestamp: page.revisions[0].timestamp,
summary: summary,
text: mblob.toString(),
token: mw.user.tokens.get('csrfToken')
}, {
success: function (result) {
if (result.error) {
setStatus([
'API error: "',
result.error.info,
'" [code: ', el('code', [result.error.code]), ']'
]);
return;
}
setStatus(['Saved. Reloading page...']);
if (/redirect=no/.test(location.href)) // XXX
location.reload();
else
location.search = location.search ? location.search + '&redirect=no' : '?redirect=no';
}
});
});
var userName = mw.user.getName();
api.get({
"action": "query",
"format": "json",
"list": "users",
"usprop": "groups",
"ususers": userName
}, { success: function(result) {
var groups = result["query"]["users"][0]["groups"]
if (groups.includes("patroller")) {
ui.patrolLine.insertBefore(el('label', [
el('input', [], {
'class': 'checkbox',
'type': 'checkbox',
'id': 'patrol',
'value': 'patrol'
}), 'Mark as patrolled?']),null);
}
}
});
ui.statusLine.insertBefore(el('input', null, {
type: 'submit',
value: 'Save'
}), ui.statusLine.firstChild);
uiWrapper.appendChild(ui);
}
});
} else if ((mw.config.get('wgPageContentModel') === 'wikitext') && ((mw.config.get('wgAction') === 'edit') || (mw.config.get('wgAction') === 'submit'))) {
if (mw.util.getParamValue('section'))
return;
var editform = document.getElementById('editform');
if (!editform || !editform.wpTextbox1 || editform.wpTextbox1.readOnly)
return;
var uiPivot = document.getElementsByClassName('wikiEditor-ui')[0];
var ui, mblob;
firstHeading.appendChild(document.createTextNode(' '));
firstHeading.appendChild(link(['♑'], function () {
if (ui && ui.parentNode)
ui.parentNode.removeChild(ui);
try {
mblob = new MarkupBlob(editform.wpTextbox1.value);
} catch (e) {
alert("Error: unable to parse page. This page is probably not a redirect.");
return;
}
currentTarget = mblob.target
ui = buildEditingUI(mblob, function () {
editform.wpSummary.value = 'Redirecting to [[' + mblob.target + ']] ([[User:Wugapodes/Capricorn|♑]])';
editform.wpTextbox1.value = mblob.toString();
mblob = null;
ui.style.display = 'none';
uiPivot.style.display = '';
});
ui.style.display = 'none';
ui.statusLine.insertBefore(el('input', null, {
type: "button",
value: "Cancel",
}, {
click: function () {
mblob = null;
ui.style.display = 'none';
uiPivot.style.display = '';
}
}), ui.statusLine.firstChild);
ui.statusLine.insertBefore(el('input', null, {
type: "submit",
value: "Check"
}), ui.statusLine.firstChild);
uiPivot.parentNode.insertBefore(ui, uiPivot);
uiPivot.style.display = 'none';
ui.style.display = '';
}, {
"class": "kephir-sagittarius-editlink",
"title": "Edit this redirect with Capricorn"
}));
var submitButton;
var inputs = editform.getElementsByTagName('input');
for (var i = 0; i < inputs.length; ++i) {
inputs[i].addEventListener('click', function (ev) {
submitButton = this;
}, false);
}
editform.addEventListener('submit', function (ev) {
if (submitButton !== editform.wpSave)
return;
if (mblob) {
ev.preventDefault();
ev.stopImmediatePropagation();
ui.doCheck(function (setStatus) {
setStatus(['Proceeding with saving...']);
var summary = setSummary(currentTarget,ui.origTarget)
editform.wpTextbox1.value = mblob.toString();
editform.wpSummary.value = summary;
mblob = null;
editform.submit();
});
}
}, false);
}
if (!window.kephirSagittariusFollowCategoryRedirects)
if ((mw.config.get('wgAction') === 'view') && (mw.config.get('wgNamespaceNumber') === wgNamespaceIds.category)) {
var pagesList = document.getElementById('mw-pages').getElementsByClassName('mw-redirect');
for (var i = 0; i < pagesList.length; ++i) {
pagesList[i].href += '?redirect=no';
}
}
}
function abortConditions() {
if (window.location.href.includes('&diff=')) {
throw 'Capricorn does not run when viewing page diffs. Please revert before editing redirect.';
}
if (mw.config.get('wgNamespaceNumber') < 0) {
throw 'Page is in a virtual namespace. Capricorn aborts.';
}
}
function Capricorn() {
$.getJSON("https://en.wikipedia.org/w/index.php?title=User:Wugapodes/Capricorn/RedirectAliases.json&action=raw&ctype=application/json", function(aliasJSON) {
$.getJSON("https://en.wikipedia.org/w/index.php?title=User:Wugapodes/Capricorn/RedirectTemplates.json&action=raw&ctype=application/json",function(templateJSON) {
mw.loader.using(['jquery.suggestions', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.action.view.redirectPage'], function () {
mainCallback(aliasJSON,templateJSON);
});
});
});
}
var api = new mw.Api();
api.get({
action: "query",
format: "json",
prop: "info",
formatversion: 2,
titles: mw.config.get('wgPageName')
}, {
success: function (result) {
try {
abortConditions();
} catch(abortMessage) {
console.info(abortMessage)
return;
}
if (result.query.pages[0].redirect || (window.location.href.includes('&redlink=1'))) {
Capricorn();
} else {
console.debug('Page is not a redirect.')
return;
}
}
});
/*</source>
 
[[Category:Wikipedia scripts]]
*/
// </nowiki>

/* Novem Linguae/Scripts/GANReviewTool.js */
// <nowiki>
// === Compiled with Novem Linguae's publish.php script ======================
$(async function() {
// === GANReviewTool.js ======================================================
// 

$(async function() {
let ganController = new GANReviewController();
await ganController.execute($, mw, location, new GANReviewWikicodeGenerator(), new GANReviewHTMLGenerator());
let garController = new GARCloserController();
await garController.execute($, mw, location, new GARCloserWikicodeGenerator(), new GARCloserHTMLGenerator());
// TODO: create an environment class such as Environment, State, Output, or GANReviewIO. It can wrap all the IO junk such as $, mw, etc. Should make it easier to mock and unit test.
// TODO: extract utility functions such as escapeRegEx into a GARReviewToolUtil class
// TODO: create Page object? could store title, wikicode, etc.
});
// 
// === modules/GANReviewController.js ======================================================

class GANReviewController {
/**
 * @param {function} $ jQuery
 * @param {Object} mw mediawiki, https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw
 * @param {Location} location https://developer.mozilla.org/en-US/docs/Web/API/Window/location
 * @param {GANReviewWikicodeGenerator} wg
 * @param {GANReviewHTMLGenerator} hg
 */
async execute($, mw, location, wg, hg) {
if ( arguments.length !== 5 ) throw new Error('Incorrect # of arguments');
this.$ = $;
this.mw = mw;
this.location = location;
this.wg = wg;
this.hg = hg;
this.ganReviewPageTitle = this.mw.config.get('wgPageName'); // includes namespace, underscores instead of spaces
this.ganReviewPageTitle = this.ganReviewPageTitle.replace(/_/g, ' '); // underscores to spaces. prevents some bugs later
if ( ! this.shouldRunOnThisPageQuickChecks(this.ganReviewPageTitle) ) return;
if ( ! await this.shouldRunOnThisPageSlowChecks() ) return;
this.displayForm();
await this.warnUserIfNotReviewCreator();
this.handleUserChangingFormType();
this.$(`#GANReviewTool-Submit`).on('click', async () => {
await this.clickSubmit();
});
}
/**
 * @private
 */
async clickSubmit() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.readFormAndSetVariables();
let hasFormValidationErrors = this.validateForm();
if ( hasFormValidationErrors ) {
return;
}
this.$(`#GANReviewTool-Form`).hide();
this.$(`#GANReviewTool-ProcessingMessage`).show();
this.editSummarySuffix = ' ([[User:Novem Linguae/Scripts/GANReviewTool|GANReviewTool]])';
this.reviewTitle = this.ganReviewPageTitle;
this.error = false;
try {
if ( this.passOrFail === 'pass' ) {
await this.doPass();
} else if ( this.passOrFail === 'fail' ) {
await this.doFail();
}
} catch(err) {
this.pushStatus('<span class="GANReviewTool-ErrorNotice">An error occurred :(</span>');
this.error = err;
}
await this.writeToLog();
if ( ! this.error ) {
this.pushStatus('Script complete. Refreshing page.');
// TODO: 1 second delay?
location.reload();
}
}
/**
 * @return {boolean} hasFormValidationErrors
 * @private
 */
validateForm() {
this.$(`.GANReviewTool-ValidationError`).hide();
let hasFormValidationErrors = false;
// if pass, a WP:GA subpage heading must be selected
if ( this.passOrFail === 'pass' && ! this.detailedTopic ) {
this.$(`#GANReviewTool-NoTopicMessage`).show();
hasFormValidationErrors = true;
}
// "Wikicode to display" text box must not contain a pipe. Prevents this kind of thing from being written to the [[WP:GA]] subpages: [[HMS Resistance (1801)|HMS Resistance (1801)|HMS ''Resistance'' (1801)]]
if ( this.$(`[name="GANReviewTool-DisplayWikicode"]`).val().includes('|') ) {
this.$(`#GANReviewTool-NoPipesMessage`).show();
hasFormValidationErrors = true;
}
return hasFormValidationErrors;
}
/**
 * @private
 */
async doPass() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.editSummary = `promote [[${this.gaTitle}]] to good article` + this.editSummarySuffix;
this.gaSubpageShortTitle = this.$(`[name="GANReviewTool-Topic"]`).val();
if ( this.needsATOP ) {
await this.processPassForGANPage();
}
await this.processPassForTalkPage();
await this.processPassForGASubPage();
}
/**
 * @private
 */
async doFail() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.editSummary = `close [[${this.gaTitle}]] good article nomination as unsuccessful` + this.editSummarySuffix;
if ( this.needsATOP ) {
await this.processFailForGANPage();
}
await this.processFailForTalkPage();
}
/**
 * @private
 */
async processFailForGANPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus('Placing {{atop}} and {{abot}} on GA review page.');
let reviewWikicode = await this.getWikicode(this.ganReviewPageTitle); // get this wikicode again, in case it changed between page load and "submit" button click
reviewWikicode = this.wg.getFailWikicodeForGANPage(reviewWikicode);
this.reviewRevisionID = await this.makeEdit(this.reviewTitle, this.editSummary, reviewWikicode);
}
/**
 * @private
 */
async processFailForTalkPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus('Deleting {{GA nominee}} from article talk page.');
this.pushStatus('Adding {{FailedGA}} or {{Article history}} to article talk page.');
let talkWikicode = await this.getWikicode(this.gaTalkTitle); // get this wikicode again, in case it changed between page load and "submit" button click
talkWikicode = this.wg.getFailWikicodeForTalkPage(talkWikicode, this.reviewTitle);
this.talkRevisionID = await this.makeEdit(this.gaTalkTitle, this.editSummary, talkWikicode);
}
/**
 * @private
 */
async processPassForTalkPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus('Deleting {{GA nominee}} from article talk page.');
this.pushStatus('Adding {{GA}} or {{Article history}} to article talk page.');
this.pushStatus('Changing WikiProject template class parameters to GA on article talk page.');
let talkWikicode = await this.getWikicode(this.gaTalkTitle); // get this wikicode again, in case it changed between page load and "submit" button click
talkWikicode = this.wg.getPassWikicodeForTalkPage(talkWikicode, this.reviewTitle, this.gaSubpageShortTitle);
this.talkRevisionID = await this.makeEdit(this.gaTalkTitle, this.editSummary, talkWikicode);
}
/**
 * @private
 */
async processPassForGASubPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus('Adding to appropriate subpage of [[WP:GA]]');
let gaSubpageLongTitle = `Wikipedia:Good articles/` + this.gaSubpageShortTitle;
let gaDisplayTitle = this.$(`[name="GANReviewTool-DisplayWikicode"]`).val();
let gaSubpageWikicode = await this.getWikicode(gaSubpageLongTitle);
gaSubpageWikicode = this.wg.getPassWikicodeForGAListPage(this.detailedTopic, gaSubpageWikicode, this.gaTitle, gaDisplayTitle);
this.gaRevisionID = await this.makeEdit(gaSubpageLongTitle, this.editSummary, gaSubpageWikicode);
}
/**
 * @private
 */
async processPassForGANPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus('Placing {{atop}} and {{abot}} on GA review page.');
let reviewWikicode = await this.getWikicode(this.ganReviewPageTitle); // get this wikicode again, in case it changed between page load and "submit" button click
reviewWikicode = this.wg.getPassWikicodeForGANPage(reviewWikicode);
this.reviewRevisionID = await this.makeEdit(this.reviewTitle, this.editSummary, reviewWikicode);
}
/**
 * @private
 */
readFormAndSetVariables() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.passOrFail = this.$(`[name="GANReviewTool-PassOrFail"]:checked`).val();
this.needsATOP = this.$(`[name="GANReviewTool-ATOPYesNo"]`).is(":checked");
this.detailedTopic = document.querySelector(`[name="GANReviewTool-Topic"]`); // TODO: change this to jquery, so less dependencies, more unit testable
this.detailedTopic = this.detailedTopic.options[this.detailedTopic.selectedIndex];
this.detailedTopic = this.detailedTopic.text;
}
/**
 * Show or hide different parts of the form depending on whether the user clicks pass or fail.
 * @private
 */
handleUserChangingFormType() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.$(`[name="GANReviewTool-PassOrFail"]`).on('change', () => {
if ( this.$(`[name="GANReviewTool-PassOrFail"]:checked`).val() === 'pass' ) {
this.$(`#GANReviewTool-PassDiv`).show();
} else {
this.$(`#GANReviewTool-PassDiv`).hide();
this.$(`#GANReviewTool-NoTopicMessage`).hide();
}
});
}
/**
 * Show a warning if viewer is not the creator of the GAN Review page. This is to help prevent accidentally closing the wrong GAN Review.
 * @private
 */
async warnUserIfNotReviewCreator() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
let pageCreator = await this.getPageCreator(this.ganReviewPageTitle);
if ( pageCreator !== this.mw.config.get('wgUserName') ) {
this.$('.GANReviewTool-NotCreatorNotice').show();
} else {
this.$('#GANReviewTool-MainForm').show();
}
this.$('#GANReviewTool-ReviewAnywayLink').on('click', () => {
this.$('.GANReviewTool-NotCreatorNotice').hide();
this.$('#GANReviewTool-MainForm').show();
});
}
/**
 * @private
 */
displayForm() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');

this.$('#mw-content-text').prepend(this.hg.getHTML(this.gaTitle));
}
/**
 * @private
 */
async shouldRunOnThisPageSlowChecks() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
// only run if this review hasn't already been closed. check for {{atop}}
let reviewWikicode = await this.getWikicode(this.ganReviewPageTitle);
if ( reviewWikicode.match(/\{\{atop/i) ) {
return false;
}
// only run if talk page has {{GA nominee}}
this.gaTitle = this.getGATitle(this.ganReviewPageTitle);
this.gaTalkTitle = this.getGATalkTitle(this.gaTitle);
let talkWikicode = await this.getWikicode(this.gaTalkTitle);
if ( this.ganReviewPageTitle !== 'User:Novem_Linguae/sandbox' && ! talkWikicode.match(/\{\{GA nominee/i) ) {
return false;
}
return true;
}
/**
 * @private
 */
async writeToLog() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
// always log no matter what. hopefully log some errors so I can fix them
this.pushStatus('Adding to log');
let username = this.mw.config.get('wgUserName');
let textToAppend = this.wg.getLogMessageToAppend(username, this.passOrFail, this.reviewTitle, this.reviewRevisionID, this.talkRevisionID, this.gaRevisionID, this.error);
await this.appendToPage('User:Novem Linguae/Scripts/GANReviewTool/GANReviewLog', this.editSummary, textToAppend);
}
/**
 * @private
 */
async getWikicode(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "parse",
"page": title,
"prop": "wikitext",
"format": "json",
};
let result = await api.post(params);
if ( result['error'] ) return '';
let wikicode = result['parse']['wikitext']['*'];
return wikicode;
}
/**
 * @private
 */
async makeEdit(title, editSummary, wikicode) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "edit",
"format": "json",
"title": title,
"text": wikicode,
"summary": editSummary,
};
let result = await api.postWithToken('csrf', params);
let revisionID = result['edit']['newrevid'];
return revisionID;
}
/**
 * @private
 */
async getPageCreator(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
action: 'query',
prop: 'revisions',
titles: title,
rvlimit: 1,
rvprop: 'user',
rvdir: 'newer',
format: 'json',
};
let result = await api.post(params);
let wikicode = result['query']['pages']
let key = Object.keys(wikicode)[0];
wikicode = wikicode[key]['revisions'][0]['user'];
return wikicode;
}
/**
 * Lets you append without getting the Wikicode first. Saves an API query.
 * @private
 */
async appendToPage(title, editSummary, wikicodeToAppend) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "edit",
"format": "json",
"title": title,
"appendtext": wikicodeToAppend,
"summary": editSummary,
};
let result = await api.postWithToken('csrf', params);
let revisionID = result['edit']['newrevid'];
return revisionID;
}
/**
 * @private
 */
pushStatus(statusToAdd) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
this.$(`#GANReviewTool-ProcessingMessage > p`).append('<br />' + statusToAdd);
}
/**
 * @private
 */
shouldRunOnThisPageQuickChecks(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
// don't run when not viewing articles
let action = this.mw.config.get('wgAction');
if ( action != 'view' ) return false;
// don't run when viewing diffs
let isDiff = this.mw.config.get('wgDiffNewId');
if ( isDiff ) return false;
let isDeletedPage = ( ! this.mw.config.get('wgCurRevisionId') );
if ( isDeletedPage ) return false;
// always run in Novem's sandbox
if ( title === 'User:Novem_Linguae/sandbox' ) return true;
// only run in talk namespace
let namespace = this.mw.config.get('wgNamespaceNumber');
let isTalkNamespace = ( namespace === 1 );
if ( ! isTalkNamespace ) return false;
// only run on pages that end in /GA##
if ( ! this.isGASubPage(title) ) return false;
return true;
}
/**
 * @private
 */
isGASubPage(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return Boolean(title.match(/\/GA\d{1,2}$/));
}
/**
 * @private
 */
getGATitle(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
title = title.replace('Talk:', '');
title = title.replace(/_/g, ' ');
title = title.match(/^[^\/]*/)[0];
return title;
}
/**
 * @private
 */
getGATalkTitle(gaTitle) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
if ( gaTitle.includes(':') ) {
return gaTitle.replace(/^([^:]*)(:.*)$/gm, '$1 talk$2');
} else {
return 'Talk:' + gaTitle;
}
}
}
// === modules/GANReviewHTMLGenerator.js ======================================================
class GANReviewHTMLGenerator {
getHTML(gaTitle) {
return `
<style>
#GANReviewTool {
border: 1px solid black;
padding: 1em;
margin-bottom: 1em;
}
#GANReviewTool h2 {
margin-top: 0;
}
#GANReviewTool strong {
text-decoration: underline;
}
#GANReviewTool code {
/* font-family: monospace; */
}
#GANReviewTool input[type="text"] {
width: 50em;
}
#GANReviewTool p {
margin-top: 1.5em;
margin-bottom: 1.5em;
line-height: 1.5em;
}
#GANReviewTool option:disabled {
font-weight: bold;
color: green;
}
#GANReviewTool-ProcessingMessage {
display: none;
}
.GANReviewTool-ValidationError {
display: none;
color: red;
font-weight: bold;
}
.GANReviewTool-NotCreatorNotice {
display: none;
color: red;
font-weight: bold;
}
.GANReviewTool-ErrorNotice {
color: red;
font-weight: bold;
}
#GANReviewTool-MainForm {
display: none;
}
</style>
<div id="GANReviewTool">
<div id="GANReviewTool-Form">
<h2>
GAN Review Tool
</h2>
<p class="GANReviewTool-NotCreatorNotice">
You are not the creator of this GAN page. <a id="GANReviewTool-ReviewAnywayLink">Click here</a> to review anyway.
</p>
<div id="GANReviewTool-MainForm">
<p>
<strong>Pass or fail?</strong><br />
<input type="radio" name="GANReviewTool-PassOrFail" value="pass" checked /> Pass
<input type="radio" name="GANReviewTool-PassOrFail" value="fail" /> Fail
</p>
<p>
<input type="checkbox" name="GANReviewTool-ATOPYesNo" value="1" checked /> Apply {{<a href="/wiki/Template:Archive_top">atop</a>}} template to GA review
</p>
<!-- if pass -->
<div id="GANReviewTool-PassDiv">
<p>
<strong>Topic, subtopic, and sub-subtopic:</strong><br />
<select name="GANReviewTool-Topic">
<option></option>
<option value="Agriculture, food and drink" disabled>==Agriculture, food, and drink==</option>
<option value="Agriculture, food and drink" disabled>===Agriculture, food, and drink===</option>
<option value="Agriculture, food and drink">=====Agriculture and farming=====</option>
<option value="Agriculture, food and drink">=====Horticulture and forestry=====</option>
<option value="Agriculture, food and drink">=====Food and drink establishments=====</option>
<option value="Agriculture, food and drink">=====Cuisines=====</option>
<option value="Agriculture, food and drink">=====Food=====</option>
<option value="Agriculture, food and drink">=====Drink=====</option>
<option value="Agriculture, food and drink">=====Food and drink companies=====</option>
<option value="Agriculture, food and drink">=====Food and drink people=====</option>
<option value="Agriculture, food and drink">=====Cookery books=====</option>
<option value="Agriculture, food and drink">=====Miscellaneous=====</option>
<option value="Art and architecture" disabled>==Art and architecture==</option>
<option value="Art and architecture" disabled>===Art===</option>
<option value="Art and architecture">=====Art=====</option>
<option value="Art and architecture">=====Artists and art organizations=====</option>
<option value="Art and architecture" disabled>===Architecture===</option>
<option value="Art and architecture">=====Architecture=====</option>
<option value="Art and architecture">=====Architecture – Bridges and tunnels=====</option>
<option value="Art and architecture">=====Architecture – Buildings=====</option>
<option value="Art and architecture">=====Architecture – Hotels and inns=====</option>
<option value="Art and architecture">=====Architecture – Houses and residences=====</option>
<option value="Art and architecture">=====Architecture – Lighthouses=====</option>
<option value="Art and architecture">=====Architecture – Memorials and monuments=====</option>
<option value="Art and architecture">=====Architecture – Museums and galleries=====</option>
<option value="Art and architecture">=====Architecture – Religious=====</option>
<option value="Art and architecture">=====Architects=====</option>
<option value="Engineering and technology" disabled>==Engineering and technology==</option>
<option value="Engineering and technology" disabled>===Computing and engineering===</option>
<option value="Engineering and technology">=====Computer-related organizations and people=====</option>
<option value="Engineering and technology">=====Cryptography=====</option>
<option value="Engineering and technology">=====Engineers and inventors=====</option>
<option value="Engineering and technology">=====Engineering technology=====</option>
<option value="Engineering and technology">=====Engineering failures and disasters=====</option>
<option value="Engineering and technology">=====Hardware, standards and protocols=====</option>
<option value="Engineering and technology">=====Power and water infrastructure=====</option>
<option value="Engineering and technology">=====Programming=====</option>
<option value="Engineering and technology">=====Software=====</option>
<option value="Engineering and technology">=====Websites and the Internet=====</option>
<option value="Engineering and technology" disabled>===Transport===</option>
<option value="Engineering and technology">=====Air transport=====</option>
<option value="Engineering and technology">=====Maritime transport=====</option>
<option value="Engineering and technology">=====Rail transport=====</option>
<option value="Engineering and technology">=====Rail bridges, tunnels, and stations=====</option>
<option value="Engineering and technology">=====Trains and locomotives=====</option>
<option value="Engineering and technology">=====Road infrastructure: Canada=====</option>
<option value="Engineering and technology">=====Road infrastructure: United States=====</option>
<option value="Engineering and technology">=====Road infrastructure: Northeastern United States=====</option>
<option value="Engineering and technology">=====Road infrastructure: Southern United States=====</option>
<option value="Engineering and technology">=====Road infrastructure: Midwestern United States=====</option>
<option value="Engineering and technology">=====Road infrastructure: Western United States=====</option>
<option value="Engineering and technology">=====Road infrastructure: Other=====</option>
<option value="Engineering and technology">=====Road transportation: Buses, vans, and paratransit=====</option>
<option value="Engineering and technology">=====Road transportation: Cars and motorcycles=====</option>
<option value="Engineering and technology">=====Road transportation: Other=====</option>
<option value="Engineering and technology">=====Transport by region=====</option>
<option value="Geography and places" disabled>==Geography and places==</option>
<option value="Geography and places" disabled>===Geography===</option>
<option value="Geography and places">=====Bodies of water and water formations=====</option>
<option value="Geography and places">=====Geographers and explorers=====</option>
<option value="Geography and places">=====General and human geography=====</option>
<option value="Geography and places">=====Islands=====</option>
<option value="Geography and places">=====Landforms=====</option>
<option value="Geography and places">=====National and state parks, nature reserves, conservation areas, and countryside routes=====</option>
<option value="Geography and places">=====Urban and historical sites=====</option>
<option value="Geography and places">=====Geography miscellanea=====</option>
<option value="Geography and places" disabled>===Places===</option>
<option value="Geography and places">=====Countries=====</option>
<option value="Geography and places">=====Africa=====</option>
<option value="Geography and places">=====Antarctica=====</option>
<option value="Geography and places">=====Asia=====</option>
<option value="Geography and places">=====Australia and the Pacific=====</option>
<option value="Geography and places">=====Europe=====</option>
<option value="Geography and places">=====Middle East=====</option>
<option value="Geography and places">=====North America=====</option>
<option>=====South America=====</option>
<option value="History" disabled>==History==</option>
<option value="History" disabled>===World history===</option>
<option value="History">=====Archaeology and archaeologists=====</option>
<option value="History">=====Historians, chroniclers and history books=====</option>
<option value="History">=====Historical figures: heads of state and heads of government=====</option>
<option value="History">=====Historical figures: politicians=====</option>
<option value="History">=====Historical figures: other=====</option>
<option value="History">=====African history=====</option>
<option value="History">=====North American history=====</option>
<option value="History">=====South American history=====</option>
<option value="History">=====Asian history=====</option>
<option value="History">=====Australian and Oceania history=====</option>
<option value="History">=====European history=====</option>
<option value="History">=====Middle Eastern history=====</option>
<option value="History">=====Global history=====</option>
<option value="History" disabled>===Royalty, nobility, and heraldry===</option>
<option value="History">=====Flags and heraldry=====</option>
<option value="History">=====Monarchs=====</option>
<option value="History">=====Royalty and nobility=====</option>
<option value="Language and literature" disabled>==Language and literature==</option>
<option value="Language and literature" disabled>===Language and literature===</option>
<option value="Language and literature">=====Alphabets and transliteration=====</option>
<option value="Language and literature">=====Ancient texts=====</option>
<option value="Language and literature">=====Biographies, autobiographies, essays, diaries, and travelogues=====</option>
<option value="Language and literature">=====Characters and fictional items=====</option>
<option value="Language and literature">=====Children's books, fairy tales, and nursery rhymes=====</option>
<option value="Language and literature">=====Comics=====</option>
<option value="Language and literature">=====Genres and literary theory=====</option>
<option value="Language and literature">=====Languages=====</option>
<option value="Language and literature">=====Linguists and philologists=====</option>
<option value="Language and literature">=====Non-fiction=====</option>
<option value="Language and literature">=====Novels=====</option>
<option value="Language and literature">=====Plays=====</option>
<option value="Language and literature">=====Poetry=====</option>
<option value="Language and literature">=====Short fiction and anthologies=====</option>
<option value="Language and literature">=====Words and linguistics=====</option>
<option value="Language and literature">=====Writers, publishers, and critics=====</option>
<option value="Mathematics" disabled>==Mathematics==</option>
<option value="Mathematics" disabled>===Mathematics and mathematicians===</option>
<option value="Mathematics">=====Mathematical concepts and topics=====</option>
<option value="Mathematics">=====Mathematical texts and artifacts=====</option>
<option value="Mathematics">=====Mathematicians=====</option>
<option value="Media and drama" disabled>==Media and drama==</option>
<option value="Media and drama" disabled>===Film===</option>
<option value="Media and drama">=====Cinema=====</option>
<option value="Media and drama">=====Film franchises, overview articles and production articles=====</option>
<option value="Media and drama">=====Pre-1910s films=====</option>
<option value="Media and drama">=====1910s films=====</option>
<option value="Media and drama">=====1920s films=====</option>
<option value="Media and drama">=====1930s films=====</option>
<option value="Media and drama">=====1940s films=====</option>
<option value="Media and drama">=====1950s films=====</option>
<option value="Media and drama">=====1960s films=====</option>
<option value="Media and drama">=====1970s films=====</option>
<option value="Media and drama">=====1980s films=====</option>
<option value="Media and drama">=====1990s films=====</option>
<option value="Media and drama">=====2000 to 2004 films=====</option>
<option value="Media and drama">=====2005 to 2009 films=====</option>
<option value="Media and drama">=====2010 to 2014 films=====</option>
<option value="Media and drama">=====2015 to 2019 films=====</option>
<option value="Media and drama">=====2020 to 2024 films=====</option>
<option value="Media and drama" disabled>===Television===</option>
<option value="Media and drama">=====Television networks and overview articles=====</option>
<option value="Media and drama">====Television series====</option>
<option value="Media and drama">=====''30 Rock''=====</option>
<option value="Media and drama">=====''Ackley Bridge''=====</option>
<option value="Media and drama">=====''Adventure Time''=====</option>
<option value="Media and drama">=====''American Dad!''=====</option>
<option value="Media and drama">=====''American Horror Story''=====</option>
<option value="Media and drama">=====''Archer''=====</option>
<option value="Media and drama">=====Arrowverse=====</option>
<option value="Media and drama">=====Avatarverse=====</option>
<option value="Media and drama">=====''Awake''=====</option>
<option value="Media and drama">=====''Battlestar Galactica''=====</option>
<option value="Media and drama">=====''Better Call Saul''=====</option>
<option value="Media and drama">=====''The Big Bang Theory''=====</option>
<option value="Media and drama">=====''Black Mirror''=====</option>
<option value="Media and drama">=====''Body of Proof''=====</option>
<option value="Media and drama">=====''BoJack Horseman''=====</option>
<option value="Media and drama">=====''Breaking Bad''=====</option>
<option value="Media and drama">=====''Buffy the Vampire Slayer''=====</option>
<option value="Media and drama">=====''Casualty''=====</option>
<option value="Media and drama">=====''Cheers''=====</option>
<option value="Media and drama">=====''Chuck''=====</option>
<option value="Media and drama">=====''Cold Feet''=====</option>
<option value="Media and drama">=====''Community''=====</option>
<option value="Media and drama">=====''Coronation Street''=====</option>
<option value="Media and drama">=====''Daredevil''=====</option>
<option value="Media and drama">=====''Desperate Housewives''=====</option>
<option value="Media and drama">=====''Dexter''=====</option>
<option value="Media and drama">=====''Doctor Who'' series=====</option>
<option value="Media and drama">=====''Doctor Who'' episodes=====</option>
<option value="Media and drama">=====''Doctors''=====</option>
<option value="Media and drama">=====''EastEnders''=====</option>
<option value="Media and drama">=====''Ed, Edd n Eddy''=====</option>
<option value="Media and drama">=====''Emmerdale''=====</option>
<option value="Media and drama">=====''Family Guy''=====</option>
<option value="Media and drama">=====''Friends''=====</option>
<option value="Media and drama">=====''Fringe'' series=====</option>
<option value="Media and drama">=====''Fringe'' episodes=====</option>
<option value="Media and drama">=====''Futurama''=====</option>
<option value="Media and drama">=====''Game of Thrones''=====</option>
<option value="Media and drama">=====''Glee'' series=====</option>
<option value="Media and drama">=====''Glee'' episodes=====</option>
<option value="Media and drama">=====''The Good Place''=====</option>
<option value="Media and drama">=====''Gossip Girl''=====</option>
<option value="Media and drama">=====''Grey's Anatomy'' series=====</option>
<option value="Media and drama">=====''Grey's Anatomy'' episodes=====</option>
<option value="Media and drama">=====''Hawaii Five-0 (2010 TV series)''=====</option>
<option value="Media and drama">=====''The Hills''=====</option>
<option value="Media and drama">=====''Home and Away''=====</option>
<option value="Media and drama">=====''Holby City''=====</option>
<option value="Media and drama">=====''Hollyoaks''=====</option>
<option value="Media and drama">=====''Homicide: Life on the Street''=====</option>
<option value="Media and drama">=====''House''=====</option>
<option value="Media and drama">=====''House of Cards''=====</option>
<option value="Media and drama">=====''The House of Flowers''=====</option>
<option value="Media and drama">=====''Inside No. 9''=====</option>
<option value="Media and drama">=====''Last Week Tonight with John Oliver''=====</option>
<option value="Media and drama">=====''Law & Order: Special Victims Unit''=====</option>
<option value="Media and drama">=====''Lost'' series=====</option>
<option value="Media and drama">=====''Lost'' episodes=====</option>
<option value="Media and drama">=====''Mad Men''=====</option>
<option value="Media and drama">=====''Magnum P.I.''=====</option>
<option value="Media and drama">=====Marvel Cinematic Universe=====</option>
<option value="Media and drama">=====''Millennium''=====</option>
<option value="Media and drama">=====''Modern Family''=====</option>
<option value="Media and drama">=====''Monk''=====</option>
<option value="Media and drama">=====''My Little Pony: Friendship Is Magic''=====</option>
<option value="Media and drama">=====''Neighbours''=====</option>
<option value="Media and drama">=====''Neon Genesis Evangelion''=====</option>
<option value="Media and drama">=====''The Office'' series=====</option>
<option value="Media and drama">=====''The Office'' episodes=====</option>
<option value="Media and drama">=====''Once Upon a Time''=====</option>
<option value="Media and drama">=====''Parks and Recreation''=====</option>
<option value="Media and drama">=====''Phineas and Ferb''=====</option>
<option value="Media and drama">=====''Psych''=====</option>
<option value="Media and drama">=====''Rugrats''=====</option>
<option value="Media and drama">=====''Sanctuary''=====</option>
<option value="Media and drama">=====''Seinfeld''=====</option>
<option value="Media and drama">=====''Sesame Street'' series and co-productions=====</option>
<option value="Media and drama">=====''The Simpsons'' series=====</option>
<option value="Media and drama">=====''The Simpsons'' episodes=====</option>
<option value="Media and drama">=====''Skins''=====</option>
<option value="Media and drama">=====''Smallville''=====</option>
<option value="Media and drama">=====''South Park'' series=====</option>
<option value="Media and drama">=====''South Park'' episodes=====</option>
<option value="Media and drama">=====''The Spectacular Spider-Man''=====</option>
<option value="Media and drama">=====''SpongeBob SquarePants''=====</option>
<option value="Media and drama">=====''Spooks''=====</option>
<option value="Media and drama">=====''Stargate''=====</option>
<option value="Media and drama">=====''Star Trek'' series=====</option>
<option value="Media and drama">=====''Star Trek'' series episodes=====</option>
<option value="Media and drama">=====''Supernatural''=====</option>
<option value="Media and drama">=====''Thunderbirds''=====</option>
<option value="Media and drama">=====''Torchwood''=====</option>
<option value="Media and drama">=====''Twin Peaks''=====</option>
<option value="Media and drama">=====''Ugly Americans''=====</option>
<option value="Media and drama">=====''Veronica Mars''=====</option>
<option value="Media and drama">=====''The Walking Dead''=====</option>
<option value="Media and drama">=====''WandaVision''=====</option>
<option value="Media and drama">=====''The West Wing''=====</option>
<option value="Media and drama">=====''White Collar''=====</option>
<option value="Media and drama">=====''Will & Grace''=====</option>
<option value="Media and drama">=====''The X-Files'' series=====</option>
<option value="Media and drama">=====''The X-Files'' episodes=====</option>
<option value="Media and drama">=====Other television series, 1950s debuts=====</option>
<option value="Media and drama">=====Other television series, 1960s debuts=====</option>
<option value="Media and drama">=====Other television series, 1970s debuts=====</option>
<option value="Media and drama">=====Other television series, 1980s debuts=====</option>
<option value="Media and drama">=====Other television series, 1990s debuts=====</option>
<option value="Media and drama">=====Other television series, 2000s debuts=====</option>
<option value="Media and drama">=====Other television series, 2010s debuts=====</option>
<option value="Media and drama">=====Other television series, 2020s debuts=====</option>
<option value="Media and drama">=====Other television seasons and related articles=====</option>
<option value="Media and drama">=====Other episodes and specials=====</option>
<option value="Media and drama" disabled>===Media and drama===</option>
<option value="Media and drama">=====Actors, directors, models, performers, and celebrities=====</option>
<option value="Media and drama">=====Animation=====</option>
<option value="Media and drama">=====Fictional characters and technologies=====</option>
<option value="Media and drama">=====Radio=====</option>
<option value="Media and drama">=====Theatre, musical theatre, dance, and opera=====</option>
<option value="Music" disabled>==Music==</option>
<option value="Music" disabled>===Albums===</option>
<option value="Music">=====1950 to 1969 albums=====</option>
<option value="Music">=====1970 to 1979 albums=====</option>
<option value="Music">=====1980 to 1989 albums=====</option>
<option value="Music">=====1990 to 1994 albums=====</option>
<option value="Music">=====1995 to 1999 albums=====</option>
<option value="Music">=====2000 to 2004 albums=====</option>
<option value="Music">=====2005 to 2009 albums=====</option>
<option value="Music">=====2010 to 2014 albums=====</option>
<option value="Music">=====2015 to 2019 albums=====</option>
<option value="Music">=====2020 to 2024 albums=====</option>
<option value="Music">=====Soundtracks=====</option>
<option value="Music">=====Video albums=====</option>
<option value="Music" disabled>===Classical compositions===</option>
<option value="Music">=====Classical compositions=====</option>
<option value="Music" disabled>===Songs===</option>
<option value="Music">=====Pre-1900 songs=====</option>
<option value="Music">=====1900 to 1959 songs=====</option>
<option value="Music">=====1960 to 1969 songs=====</option>
<option value="Music">=====1970 to 1979 songs=====</option>
<option value="Music">=====1980 to 1989 songs=====</option>
<option value="Music">=====1990 to 1999 songs=====</option>
<option value="Music">=====2000 to 2004 songs=====</option>
<option value="Music">=====2005 to 2006 songs=====</option>
<option value="Music">=====2007 to 2008 songs=====</option>
<option value="Music">=====2009 songs=====</option>
<option value="Music">=====2010 songs=====</option>
<option value="Music">=====2011 songs=====</option>
<option value="Music">=====2012 songs=====</option>
<option value="Music">=====2013 songs=====</option>
<option value="Music">=====2014 songs=====</option>
<option value="Music">=====2015 songs=====</option>
<option value="Music">=====2016 songs=====</option>
<option value="Music">=====2017 songs=====</option>
<option value="Music">=====2018 songs=====</option>
<option value="Music">=====2019 songs=====</option>
<option value="Music">=====2020 songs=====</option>
<option value="Music">=====2021 songs=====</option>
<option value="Music">=====2022 songs=====</option>
<option value="Music" disabled>===Other music articles===</option>
<option value="Music">=====Music awards=====</option>
<option value="Music">=====Music by nation, people, region, or country=====</option>
<option value="Music">=====Music genres, music styles, music eras=====</option>
<option value="Music">=====Musical theory, musical instruments, and music techniques=====</option>
<option value="Music">=====Music businesses and events=====</option>
<option value="Music">=====Performers, groups, composers, and other music-related people=====</option>
<option value="Natural sciences" disabled>==Natural sciences==</option>
<option value="Natural sciences" disabled>===Biology and medicine===</option>
<option value="Natural sciences">====Biology====</option>
<option value="Natural sciences">=====Anatomy=====</option>
<option value="Natural sciences">=====Biologists=====</option>
<option value="Natural sciences">=====Biology books=====</option>
<option value="Natural sciences">=====Ecology=====</option>
<option value="Natural sciences">=====Evolution and reproduction=====</option>
<option value="Natural sciences">=====Molecular and cellular biology=====</option>
<option value="Natural sciences">=====Miscellaneous biology=====</option>
<option value="Natural sciences">====Medicine====</option>
<option value="Natural sciences">=====Medicine books=====</option>
<option value="Natural sciences">=====Diseases and medical conditions=====</option>
<option value="Natural sciences">=====History of medicine=====</option>
<option value="Natural sciences">=====Medical people and institutions=====</option>
<option value="Natural sciences">=====Medical procedures=====</option>
<option value="Natural sciences">====Pharmacology====</option>
<option value="Natural sciences">=====Vaccines=====</option>
<option value="Natural sciences">=====Drug classes and individual drugs=====</option>
<option value="Natural sciences">=====Pharmacology miscellanea=====</option>
<option value="Natural sciences">====Viruses====</option>
<option value="Natural sciences">====Organisms====</option>
<option value="Natural sciences">=====Bacterial species=====</option>
<option value="Natural sciences">=====Protists=====</option>
<option value="Natural sciences">=====Fungi=====</option>
<option value="Natural sciences">=====Plants=====</option>
<option value="Natural sciences">=====Animals=====</option>
<option value="Natural sciences">======Mammals and other synapsids======</option>
<option value="Natural sciences">======Birds======</option>
<option value="Natural sciences">======Non-avian dinosaurs======</option>
<option value="Natural sciences">======Reptiles and amphibians======</option>
<option value="Natural sciences">======Fish======</option>
<option value="Natural sciences">======Arthropods======</option>
<option value="Natural sciences">======Other invertebrates======</option>
<option value="Natural sciences">======Animal domestic breeds, types, and individuals======</option>
<option value="Natural sciences" disabled>===Chemistry and materials science===</option>
<option value="Natural sciences">=====Areas of chemistry theory=====</option>
<option value="Natural sciences">=====Chemistry books=====</option>
<option value="Natural sciences">=====Types of chemical analyses=====</option>
<option value="Natural sciences">=====Types of chemical transformations=====</option>
<option value="Natural sciences">=====Named reactions=====</option>
<option value="Natural sciences">=====Classes of chemical compounds and materials=====</option>
<option value="Natural sciences">=====Chemical compounds and materials=====</option>
<option value="Natural sciences">=====Periodic table groups and periods=====</option>
<option value="Natural sciences">=====Elements=====</option>
<option value="Natural sciences">=====Chemistry and materials science organizations=====</option>
<option value="Natural sciences">=====Chemists and materials scientists=====</option>
<option value="Natural sciences">=====Chemistry miscellanea=====</option>
<option value="Natural sciences">=====Materials science miscellanea=====</option>
<option value="Natural sciences" disabled>===Earth science===</option>
<option value="Natural sciences">====Geology====</option>
<option value="Natural sciences">=====Geology and geophysics=====</option>
<option value="Natural sciences">=====Geologists, geophysicists and mineralogists=====</option>
<option value="Natural sciences">=====Mineralogy=====</option>
<option value="Natural sciences">=====Earthquakes and similar natural disasters=====</option>
<option value="Natural sciences">====Meteorology====</option>
<option value="Natural sciences">=====Climate=====</option>
<option value="Natural sciences">=====Climate change=====</option>
<option value="Natural sciences">=====Meteorological observatories=====</option>
<option value="Natural sciences">=====Storm sciences, tropical cyclone seasons, and storm effects=====</option>
<option value="Natural sciences">=====Tropical cyclones: Atlantic=====</option>
<option value="Natural sciences">=====Tropical cyclones: Eastern Pacific=====</option>
<option value="Natural sciences">=====Tropical cyclones: Northwestern Pacific=====</option>
<option value="Natural sciences">=====Tropical cyclones: Southern Pacific and the Indian Ocean=====</option>
<option value="Natural sciences">=====Weather=====</option>
<option value="Natural sciences">=====Severe weather and winter storms=====</option>
<option value="Natural sciences" disabled>===Physics and astronomy===</option>
<option value="Natural sciences">====Physics====</option>
<option value="Natural sciences">=====Physics=====</option>
<option value="Natural sciences">=====Physicists=====</option>
<option value="Natural sciences">====Astronomy====</option>
<option value="Natural sciences">=====Space travelers=====</option>
<option value="Natural sciences">=====Astronomy and astrophysics=====</option>
<option value="Natural sciences">=====Astronomers and astrophysicists=====</option>
<option value="Natural sciences">=====Solar system=====</option>
<option value="Natural sciences">=====Constellations and asterisms=====</option>
<option value="Natural sciences">=====Stars, galaxies and extrasolar objects=====</option>
<option value="Natural sciences">=====Rocketry and spaceflight=====</option>
<option value="Natural sciences">=====Astronomy miscellanea=====</option>
<option value="Philosophy and religion" disabled>==Philosophy and religion==</option>
<option value="Philosophy and religion" disabled>===Philosophy===</option>
<option value="Philosophy and religion">=====Divinities and protohistoric figures=====</option>
<option value="Philosophy and religion">=====Myths, mythology, and miracles=====</option>
<option value="Philosophy and religion">=====Philosophies and philosophical movements=====</option>
<option value="Philosophy and religion">=====Philosophical doctrines, teachings, texts, events, and symbols=====</option>
<option value="Philosophy and religion">=====Philosophers=====</option>
<option value="Philosophy and religion" disabled>===Religion===</option>
<option value="Philosophy and religion">=====Religions and religious movements=====</option>
<option value="Philosophy and religion">=====Religious congregations, denominations, and organizations=====</option>
<option value="Philosophy and religion">=====Religious doctrines, teachings, texts, events, and symbols=====</option>
<option value="Philosophy and religion">=====Religious figures=====</option>
<option value="Social sciences and society" disabled>==Social sciences and society==</option>
<option value="Social sciences and society" disabled>===Culture, sociology, and psychology===</option>
<option value="Social sciences and society">=====Culture and cultural studies=====</option>
<option value="Social sciences and society">=====Cultural symbols=====</option>
<option value="Social sciences and society">=====Internet culture=====</option>
<option value="Social sciences and society">=====Cultural organizations and events=====</option>
<option value="Social sciences and society">=====Ethnic groups=====</option>
<option value="Social sciences and society">=====Psychology and psychologists=====</option>
<option value="Social sciences and society">=====Anthropology, anthropologists, sociology and sociologists=====</option>
<option value="Social sciences and society">=====Globalization=====</option>
<option value="Social sciences and society" disabled>===Education===</option>
<option value="Social sciences and society">=====Educational institutions=====</option>
<option value="Social sciences and society">=====Educators=====</option>
<option value="Social sciences and society">=====Education miscellanea=====</option>
<option value="Social sciences and society" disabled>===Economics and business===</option>
<option value="Social sciences and society">=====Advertising and marketing=====</option>
<option value="Social sciences and society">=====Businesspeople=====</option>
<option value="Social sciences and society">=====Businesses and organizations=====</option>
<option value="Social sciences and society">=====Economics=====</option>
<option value="Social sciences and society">=====Numismatics and currencies=====</option>
<option value="Social sciences and society" disabled>===Law===</option>
<option value="Social sciences and society">=====Case law and litigation=====</option>
<option value="Social sciences and society">=====Constitutional law=====</option>
<option value="Social sciences and society">=====Criminal justice, law enforcement, and ethics=====</option>
<option value="Social sciences and society">=====Criminals, crimes, allegations, and victims=====</option>
<option value="Social sciences and society">=====Domestic law=====</option>
<option value="Social sciences and society">=====International laws and treaties=====</option>
<option value="Social sciences and society">=====Lawyers, judges and legal academics=====</option>
<option value="Social sciences and society">=====Legal institutions, publications, and buildings=====</option>
<option value="Social sciences and society">=====Legislation and statutory law=====</option>
<option value="Social sciences and society">=====Law miscellanea=====</option>
<option value="Social sciences and society" disabled>===Magazines and print journalism===</option>
<option value="Social sciences and society">=====Journalism and newspapers=====</option>
<option value="Social sciences and society">=====Magazines and journals=====</option>
<option value="Social sciences and society" disabled>===Politics and government===</option>
<option value="Social sciences and society">=====Heads of state and heads of government=====</option>
<option value="Social sciences and society">=====Spouses of heads of state and heads of government=====</option>
<option value="Social sciences and society">=====Intelligence and espionage=====</option>
<option value="Social sciences and society">=====International organizations=====</option>
<option value="Social sciences and society">=====National non-governmental organizations=====</option>
<option value="Social sciences and society">=====Political and governmental institutions=====</option>
<option value="Social sciences and society">=====Political districts, direction and governance=====</option>
<option value="Social sciences and society">=====Political events and elections=====</option>
<option value="Social sciences and society">=====Political figures=====</option>
<option value="Social sciences and society">=====Political issues, theory and analysis=====</option>
<option value="Social sciences and society">=====Political parties and movements=====</option>
<option value="Sports and recreation" disabled>==Sports and recreation==</option>
<option value="Sports and recreation" disabled>===Football===</option>
<option value="Sports and recreation">=====American football teams, events, seasons, concepts=====</option>
<option value="Sports and recreation">=====American football people=====</option>
<option value="Sports and recreation">=====Association football teams, events, and concepts=====</option>
<option value="Sports and recreation">=====Association football people=====</option>
<option value="Sports and recreation">=====Australian rules and Gaelic football=====</option>
<option value="Sports and recreation">=====Canadian football=====</option>
<option value="Sports and recreation">=====Rugby and rugby league football=====</option>
<option value="Sports and recreation" disabled>===Baseball===</option>
<option value="Sports and recreation">=====Baseball teams, venues, events, and concepts=====</option>
<option value="Sports and recreation">=====Baseball people=====</option>
<option value="Sports and recreation" disabled>===Basketball===</option>
<option value="Sports and recreation">=====Basketball teams, venues and events=====</option>
<option value="Sports and recreation">=====Basketball people=====</option>
<option value="Sports and recreation" disabled>===Cricket===</option>
<option value="Sports and recreation">=====Cricket teams, venues and events=====</option>
<option value="Sports and recreation">=====Cricket people=====</option>
<option value="Sports and recreation" disabled>===Hockey===</option>
<option value="Sports and recreation">=====Field hockey=====</option>
<option value="Sports and recreation">=====Ice hockey teams, venues and events=====</option>
<option value="Sports and recreation">=====Ice hockey people=====</option>
<option value="Sports and recreation" disabled>===Motorsport===</option>
<option value="Sports and recreation">=====Races and seasons=====</option>
<option value="Sports and recreation">=====Racers, racecars, and tracks=====</option>
<option disabled>===Pro wrestling===</option>
<option value="Sports and recreation">=====Professional wrestling events=====</option>
<option value="Sports and recreation">=====Professional wrestling groups=====</option>
<option value="Sports and recreation">=====Professional wrestling people=====</option>
<option value="Sports and recreation">=====Professional wrestling championships=====</option>
<option value="Sports and recreation">=====Professional wrestling (other)=====</option>
<option value="Sports and recreation" disabled>===Recreation===</option>
<option value="Sports and recreation">=====Board, card, and role-playing games=====</option>
<option value="Sports and recreation">=====Chess=====</option>
<option value="Sports and recreation">=====Climbing=====</option>
<option value="Sports and recreation">=====Diving=====</option>
<option value="Sports and recreation">=====Poker=====</option>
<option value="Sports and recreation">=====Toys=====</option>
<option value="Sports and recreation">=====Stadiums, public parks, and amusements=====</option>
<option value="Sports and recreation">=====Yoga=====</option>
<option value="Sports and recreation">=====Zoos and public aquariums=====</option>
<option value="Sports and recreation" disabled>===Multi-sport event===</option>
<option value="Sports and recreation">=====Olympics=====</option>
<option value="Sports and recreation">=====Summer Olympics=====</option>
<option value="Sports and recreation">=====Winter Olympics=====</option>
<option value="Sports and recreation">=====Paralympics=====</option>
<option value="Sports and recreation">=====Other multi-sport events=====</option>
<option value="Sports and recreation" disabled>===Other sports===</option>
<option value="Sports and recreation">=====Archery=====</option>
<option value="Sports and recreation">=====Badminton=====</option>
<option value="Sports and recreation">=====Cue sports=====</option>
<option value="Sports and recreation">=====Curling=====</option>
<option value="Sports and recreation">=====Cycling=====</option>
<option value="Sports and recreation">=====Darts=====</option>
<option value="Sports and recreation">=====Equestrianism/Horse racing=====</option>
<option value="Sports and recreation">=====Fencing=====</option>
<option value="Sports and recreation">=====Goalball=====</option>
<option value="Sports and recreation">=====Golf=====</option>
<option value="Sports and recreation">=====Gymnastics=====</option>
<option value="Sports and recreation">=====Handball=====</option>
<option value="Sports and recreation">=====Lacrosse=====</option>
<option value="Sports and recreation">=====Mixed martial arts, martial arts, and boxing=====</option>
<option value="Sports and recreation">=====Netball=====</option>
<option value="Sports and recreation">=====Rowing=====</option>
<option value="Sports and recreation">=====Running, track and field=====</option>
<option value="Sports and recreation">=====Shooting=====</option>
<option value="Sports and recreation">=====Skating=====</option>
<option value="Sports and recreation">=====Skiing=====</option>
<option value="Sports and recreation">=====Snowboarding=====</option>
<option value="Sports and recreation">=====Softball=====</option>
<option value="Sports and recreation">=====Squash=====</option>
<option value="Sports and recreation">=====Swimming and water sports=====</option>
<option value="Sports and recreation">=====Table tennis=====</option>
<option value="Sports and recreation">=====Tennis=====</option>
<option value="Sports and recreation">=====Volleyball=====</option>
<option value="Sports and recreation">=====Sports mascots and supporters=====</option>
<option value="Sports and recreation">=====Multiple sports=====</option>
<option value="Sports and recreation">=====Sports miscellanea=====</option>
<option value="Video games" disabled>==Video games==</option>
<option value="Video games" disabled>===Video games===</option>
<option value="Video games">=====Early video games=====</option>
<option value="Video games">=====1970s video games=====</option>
<option value="Video games">=====1980–84 video games=====</option>
<option value="Video games">=====1985–89 video games=====</option>
<option value="Video games">=====1990–94 video games=====</option>
<option value="Video games">=====1995–99 video games=====</option>
<option value="Video games">=====2000–04 video games=====</option>
<option value="Video games">=====2005–09 video games=====</option>
<option value="Video games">=====2010–14 video games=====</option>
<option value="Video games">=====2015–19 video games=====</option>
<option value="Video games">=====2020–24 video games=====</option>
<option value="Video games">=====Cancelled video games=====</option>
<option value="Video games">=====Video game series=====</option>
<option value="Video games">=====Video game characters=====</option>
<option value="Video games">=====Video game genres=====</option>
<option value="Video games">=====Video game systems and services=====</option>
<option value="Video games">=====Video game history and development=====</option>
<option value="Video games">=====Video game industry and developers=====</option>
<option value="Video games">=====Video game terms and game elements=====</option>
<option value="Video games">=====Video game miscellanea=====</option>
<option value="Warfare" disabled>==Warfare==</option>
<option value="Warfare" disabled>===Armies and military units===</option>
<option value="Warfare">====Air force====</option>
<option value="Warfare">====Army====</option>
<option value="Warfare">=====Australian army=====</option>
<option value="Warfare">=====United States and Confederate armies=====</option>
<option value="Warfare">====Navy====</option>
<option value="Warfare">====Other====</option>
<option value="Warfare" disabled>===Battles, exercises, and conflicts===</option>
<option value="Warfare">====Ancient and classical history (before 500)====</option>
<option value="Warfare">====Middle Ages (500–1499)====</option>
<option value="Warfare">====Early modern period (1500–1799)====</option>
<option value="Warfare">====American Revolutionary War (1775–1783)====</option>
<option value="Warfare">====French Revolutionary and Napoleonic Wars (1792–1815)====</option>
<option value="Warfare">====Long nineteenth century (1800–1914)====</option>
<option value="Warfare">====World War I and interwar (1914–1939)====</option>
<option value="Warfare">====World War II (1939–1945)====</option>
<option value="Warfare">====Post-World War II (1945–present)====</option>
<option value="Warfare">====Massacres, war crimes, and legal issues of warfare====</option>
<option value="Warfare" disabled>===Military aircraft===</option>
<option value="Warfare">====Aircraft technology and doctrine====</option>
<option value="Warfare">====Military aircraft====</option>
<option value="Warfare" disabled>===Military decorations and memorials===</option>
<option value="Warfare">====Awards and decorations====</option>
<option value="Warfare">====Military museums and memorials====</option>
<option value="Warfare" disabled>===Military people===</option>
<option value="Warfare">====Military people (A–C)====</option>
<option value="Warfare">====Military people (D–F)====</option>
<option value="Warfare">====Military people (G–K)====</option>
<option value="Warfare">====Military people (L–M)====</option>
<option value="Warfare">====Military people (N–R)====</option>
<option value="Warfare">====Military people (S–Z)====</option>
<option value="Warfare">====Warfare and race====</option>
<option value="Warfare" disabled>===Military ranks and positions===</option>
<option value="Warfare">====Military ranks and positions====</option>
<option value="Warfare" disabled>===Warships and naval units===</option>
<option value="Warfare">====Ship types====</option>
<option value="Warfare">====Naval technology====</option>
<option value="Warfare">====Warships====</option>
<option value="Warfare">=====Warships of Argentina=====</option>
<option value="Warfare">=====Warships of Australia=====</option>
<option value="Warfare">=====Warships of Austria-Hungary=====</option>
<option value="Warfare">=====Warships of Belgium=====</option>
<option value="Warfare">=====Warships of Brazil=====</option>
<option value="Warfare">=====Warships of Canada=====</option>
<option value="Warfare">=====Warships of Chile=====</option>
<option value="Warfare">=====Warships of China=====</option>
<option value="Warfare">=====Warships of the Confederate States of America=====</option>
<option value="Warfare">=====Warships of Croatia=====</option>
<option value="Warfare">=====Warships of Denmark=====</option>
<option value="Warfare">=====Warships of France=====</option>
<option value="Warfare">=====Warships of Germany=====</option>
<option value="Warfare">=====Warships of Greece=====</option>
<option value="Warfare">=====Warships of Iceland=====</option>
<option value="Warfare">=====Warships of India=====</option>
<option value="Warfare">=====Warships of Indonesia=====</option>
<option value="Warfare">=====Warships of Italy=====</option>
<option value="Warfare">=====Warships of Japan=====</option>
<option value="Warfare">=====Warships of Norway=====</option>
<option value="Warfare">=====Warships of Peru=====</option>
<option value="Warfare">=====Warships of Portugal=====</option>
<option value="Warfare">=====Warships of Romania=====</option>
<option value="Warfare">=====Warships of Russia and the Soviet Union=====</option>
<option value="Warfare">=====Warships of South Africa=====</option>
<option value="Warfare">=====Warships of Spain=====</option>
<option value="Warfare">=====Warships of Sweden=====</option>
<option value="Warfare">=====Warships of Turkey and the Ottoman Empire=====</option>
<option value="Warfare">=====Warships of the United Kingdom=====</option>
<option value="Warfare">=====Warships of the United States=====</option>
<option value="Warfare">=====Warships of Yugoslavia=====</option>
<option value="Warfare" disabled>===Weapons, equipment, and buildings===</option>
<option value="Warfare">====Weapons, military equipment and programs====</option>
<option value="Warfare">====Military uniforms and clothing====</option>
<option value="Warfare">====Fortifications and military installations====</option>
<option value="Warfare">====Castles====</option>
</select>
</p>
<p>
<strong>Wikicode to display when adding this to the list of good articles at [[<a href="/wiki/Wikipedia:Good_articles">WP:GA</a>]]</strong><br />
People should be in format: <code>Lastname, Firstname</code><br />
Albums, television shows, <a href="/wiki/Genus">genus</a>, <a href="/wiki/Binomial_nomenclature">species</a> should be italicized: <code>''Jeopardy''</code><br />
Television episodes should be surrounded by double quotes: <code>"Episode name"</code><br />
Parentheses at the end should not be formatted: <code>''Revolver'' (Beatles album)</code><br />
Artwork, poetry, etc. may also require special formatting<br />
More info at [[<a href="/wiki/Wikipedia:Manual_of_Style/Titles_of_works#Italics">MOS:TITLE#Italics</a>]] and [[<a href="/wiki/Wikipedia:Manual_of_Style/Titles_of_works#Quotation_marks">MOS:TITLE#Quotation marks</a>]]<br />
<input type="text" name="GANReviewTool-DisplayWikicode" value="${this.escapeHtml(gaTitle)}" />
</p>
</div>
<!-- endif -->
<p>
<button id="GANReviewTool-Submit">Submit</button>
</p>
<div id="GANReviewTool-NoTopicMessage" class="GANReviewTool-ValidationError">
You must select a topic from the combo box above.
</div>
<div id="GANReviewTool-NoPipesMessage" class="GANReviewTool-ValidationError">
"Wikicode to display" should not contain a pipe "|"
</div>
</div>
</div>
<div id="GANReviewTool-ProcessingMessage">
<p>
Processing...
</p>
</div>
</div>
`;
}
/**
 * CC BY-SA 4.0, bjornd, https://stackoverflow.com/a/6234804/3480193
 * @private
 */
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
}
// === modules/GANReviewWikicodeGenerator.js ======================================================
class GANReviewWikicodeGenerator {
getPassWikicodeForGANPage(reviewWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return this.placeATOP(reviewWikicode, 'Passed. ~~~~', 'green')
}
getPassWikicodeForTalkPage(talkWikicode, reviewTitle, topic) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// Deleting {{GA nominee}} from article talk page.
let gaPageNumber = this.getTemplateParameter(talkWikicode, 'GA nominee', 'page');
talkWikicode = this.deleteGANomineeTemplate(talkWikicode);
// Adding {{GA}} or {{Article history}} to article talk page.
// TODO: get top revision ID of main article, pass it into below functions, have it add the revision ID
let boolHasArticleHistoryTemplate = this.hasArticleHistoryTemplate(talkWikicode);
if ( boolHasArticleHistoryTemplate ) {
talkWikicode = this.updateArticleHistory(talkWikicode, topic, reviewTitle, 'listed');
} else {
talkWikicode = this.addGATemplate(talkWikicode, topic, gaPageNumber);
}
// Changing WikiProject template class parameters to GA on article talk page.
talkWikicode = this.changeWikiProjectArticleClassToGA(talkWikicode);
return talkWikicode;
}
getPassWikicodeForGAListPage(gaSubpageHeading, gaSubpageWikicode, gaTitle, gaDisplayTitle) {
if ( arguments.length !== 4 ) throw new Error('Incorrect # of arguments');
// find heading
let headingStartPosition = this.getGASubpageHeadingPosition(gaSubpageHeading, gaSubpageWikicode);
// now move down a bit, to the first line with an item. skip {{Further}}, {{#invoke:Good Articles|subsection|, etc.
headingStartPosition = this.findFirstStringAfterPosition('|subsection|\n', gaSubpageWikicode, headingStartPosition) + 13;
let headingEndPosition = this.findFirstStringAfterPosition('}}', gaSubpageWikicode, headingStartPosition);
gaDisplayTitle = gaDisplayTitle.trim();
let wikicodeToInsert = this.getWikicodeToInsert(gaTitle, gaDisplayTitle);
let insertPosition;
let startOfLine = headingStartPosition;
while ( startOfLine < headingEndPosition ) {
let endOfLine = this.findFirstStringAfterPosition('\n', gaSubpageWikicode, startOfLine);
let line = gaSubpageWikicode.slice(startOfLine, endOfLine);
let lineWithSomeFormattingRemoved = this.removeFormattingThatInterferesWithSort(line);
let displayTitleWithSomeFormattingRemoved = this.removeFormattingThatInterferesWithSort(gaDisplayTitle);
if ( ! this.aSortsLowerThanB(lineWithSomeFormattingRemoved, displayTitleWithSomeFormattingRemoved) ) {
insertPosition = startOfLine;
break;
}
startOfLine = endOfLine + 1;
}
if ( ! insertPosition ) {
insertPosition = headingEndPosition;
}
return this.insertStringIntoStringAtPosition(gaSubpageWikicode, wikicodeToInsert, insertPosition);
}
getFailWikicodeForGANPage(reviewWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return this.placeATOP(reviewWikicode, 'Unsuccessful. ~~~~', 'red');
}
getFailWikicodeForTalkPage(talkWikicode, reviewTitle) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
// Deleting {{GA nominee}} from article talk page.
let topic = this.getTopicFromGANomineeTemplate(talkWikicode);
let gaPageNumber = this.getTemplateParameter(talkWikicode, 'GA nominee', 'page');
talkWikicode = this.deleteGANomineeTemplate(talkWikicode);
// Adding {{FailedGA}} or {{Article history}} to article talk page.
// TODO: get top revision ID of main article, pass it into below functions, have it add the revision ID
let boolHasArticleHistoryTemplate = this.hasArticleHistoryTemplate(talkWikicode);
if ( boolHasArticleHistoryTemplate ) {
talkWikicode = this.updateArticleHistory(talkWikicode, topic, reviewTitle, 'failed');
} else {
talkWikicode = this.addFailedGATemplate(talkWikicode, topic, gaPageNumber);
}
return talkWikicode;
}
getLogMessageToAppend(username, passOrFail, reviewTitle, reviewRevisionID, talkRevisionID, gaRevisionID, error) {
if ( arguments.length !== 7 ) throw new Error('Incorrect # of arguments');
let textToAppend = `\n* `;
if ( error ) {
textToAppend += `<span style="color: red; font-weight: bold;">ERROR:</span> ${error}. `
}
textToAppend += `[[User:${username}|${username}]] ${passOrFail}ed [[${reviewTitle}]] at ~~~~~. `;
if ( reviewRevisionID ) {
textToAppend += `[[Special:Diff/${reviewRevisionID}|[Atop]]]`;
}
if ( talkRevisionID ) {
textToAppend += `[[Special:Diff/${talkRevisionID}|[Talk]]]`;
}
if ( gaRevisionID ) {
textToAppend += `[[Special:Diff/${gaRevisionID}|[List]]]`;
}
return textToAppend;
}
/**
 * @private
 */
getWikicodeToInsert(gaTitle, gaDisplayTitle) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
if ( gaDisplayTitle === gaTitle ) { // use a non-piped wikilink, when possible
return `[[${gaTitle}]]\n`;
} else if ( gaDisplayTitle === `''${gaTitle}''` ) { // put italics on the outside, when possible
return `''[[${gaTitle}]]''\n`;
} else if ( gaDisplayTitle === `"${gaTitle}"` ) { // put double quotes on the outside, when possible
return `"[[${gaTitle}]]"\n`;
} else {
return `[[${gaTitle}|${gaDisplayTitle}]]\n`;
}
}
/**
 * @private
 */
placeATOP(wikicode, result, color) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let colorCode = '';
switch ( color ) {
case 'green':
colorCode = 'g';
break;
case 'red':
colorCode = 'r';
break;
}
// place top piece after first H2, if it exists
let prependText =
`{{atop${colorCode}
| status = 
| result = ${result}
}}`;
let hasH2 = wikicode.match(/^==[^=]+==$/m);
if ( hasH2 ) {
wikicode = wikicode.replace(/^(.*?==[^=]+==\n)(.*)$/s, '$1' + prependText + '\n$2');
} else {
wikicode = prependText + "\n" + wikicode;
}
// place bottom piece at end
let appendText = `{{abot}}`;
wikicode = wikicode.trim();
wikicode += `\n${appendText}\n`;
return wikicode;
}
/**
 * @private
 */
getTopicFromGANomineeTemplate(talkWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let topic = this.getTemplateParameter(talkWikicode, 'GA nominee', 'topic');
if ( ! topic ) {
topic = this.getTemplateParameter(talkWikicode, 'GA nominee', 'subtopic');
}
return topic;
}
/**
 * @private
 */
getTemplateParameter(wikicode, templateName, parameterName) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
templateName = this.regExEscape(templateName);
parameterName = this.regExEscape(parameterName);
let regex = new RegExp(`\\{\\{${templateName}[^\\}]+\\|${parameterName}\\s*=\\s*([^\\}\\|]+)\\s*[^\\}]*\\}\\}`, 'i');
let parameterValue = wikicode.match(regex)
if ( Array.isArray(parameterValue) && parameterValue[1] !== undefined ) {
return parameterValue[1].trim();
} else {
return null;
}
}
/**
 * CC BY-SA 4.0, coolaj86, https://stackoverflow.com/a/6969486/3480193
 * @private
 */
regExEscape(string) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
 * @private
 */
deleteGANomineeTemplate(talkWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return talkWikicode.replace(/\{\{GA nominee[^\}]+\}\}\n?/i, '');
}
/**
 * @private
 */
addGATemplate(talkWikicode, topic, gaPageNumber) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let codeToAdd = `{{GA|~~~~~|topic=${topic}|page=${gaPageNumber}}}\n`;
return this.addTemplateInCorrectMOSTalkOrderPosition(talkWikicode, codeToAdd);
}
/**
 * @private
 */
addFailedGATemplate(talkWikicode, topic, gaPageNumber) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let codeToAdd = `{{FailedGA|~~~~~|topic=${topic}|page=${gaPageNumber}}}\n`;
return this.addTemplateInCorrectMOSTalkOrderPosition(talkWikicode, codeToAdd);
}
/**
 * @private
 */
addTemplateInCorrectMOSTalkOrderPosition(talkWikicode, codeToAdd) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let templateName = this.getFirstTemplateNameFromWikicode(codeToAdd);
let templatesThatGoBefore;
switch ( templateName ) {
case 'FailedGA':
case 'GA':
templatesThatGoBefore = ['GA nominee', 'Featured article candidates', 'Peer review', 'Skip to talk', 'Talk header', 'Talkheader', 'Talk page header', 'Vital article', 'Ds/talk notice', 'Gs/talk notice', 'BLP others', 'Calm', 'Censor', 'Controversial', 'Not a forum', 'FAQ', 'Round in circles', 'American English', 'British English']; // [[MOS:TALKORDER]]
break;
default:
throw new Error('addTemplateInCorrectMOSTalkOrderPosition: Supplied template is not in dictionary. Unsure where to place it.');
}
return this.addWikicodeAfterTemplates(talkWikicode, templatesThatGoBefore, codeToAdd);
}
/**
 * @private
 */
getFirstTemplateNameFromWikicode(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let match = wikicode.match(/(?<=\{\{)[^\|\}]+/)
if ( ! match ) {
throw new Error('getFirstTemplateNameFromWikicode: No template found in Wikicode.');
}
return match[0];
}
/**
 * Search algorithm looks for \n after the searched templates. If not present, it will not match.
 * @param {string[]} templates
 * @private
 */
addWikicodeAfterTemplates(wikicode, templates, codeToAdd) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
/* Started to write a lexer that would solve the edge case of putting the {{GA}} template too low when the [[MOS:TALKORDER]] is incorrect. It's a lot of work though. Pausing for now.
// Note: the MOS:TALKORDER $templates variable is fed to us as a parameter
let whitespace = ["\t", "\n", " "];
let lastTemplateNameBuffer = '';
let currentTemplateNameBuffer = '';
let templateNestingCount = 0;
for ( i = 0; i < wikicode.length; i++ ) {
let toCheck = wikicode.slice(i);
if ( toCheck.startsWith('{{') {
templateNestingCount++;
} else if ( toCheck.startsWith('}}') ) {
templateNestingCount--;
}
// TODO: need to build the templateNameBuffer. need to look for termination characters | or }
*/
let insertPosition = 0;
for ( let template of templates ) {
// TODO: handle nested templates
let regex = new RegExp(`{{${this.regExEscape(template)}[^\\}]*}}\\n`, 'ig');
let endOfTemplatePosition = this.getEndOfStringPositionOfLastMatch(wikicode, regex);
if ( endOfTemplatePosition > insertPosition ) {
insertPosition = endOfTemplatePosition;
}
}
return this.insertStringIntoStringAtPosition(wikicode, codeToAdd, insertPosition);
}
/**
 * @param {RegExp} regex /g flag must be set
 * @returns {number} endOfStringPosition Returns zero if not found
 * @private
 */
getEndOfStringPositionOfLastMatch(haystack, regex) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let matches = [...haystack.matchAll(regex)];
let hasMatches = matches.length;
if ( hasMatches ) {
let lastMatch = matches[matches.length - 1];
let lastMatchStartPosition = lastMatch['index'];
let lastMatchStringLength = lastMatch[0].length;
let lastMatchEndPosition = lastMatchStartPosition + lastMatchStringLength;
return lastMatchEndPosition;
}
return 0;
}
/**
 * @private
 */
changeWikiProjectArticleClassToGA(talkWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
// replace existing |class=
talkWikicode = talkWikicode.replace(/(\|\s*class\s*=\s*)(a|b|c|start|stub|list|fa|fl)?(?=[\}\s\|])/gi, '$1GA');
// add |class= to {{WikiProject}} templates containing no parameters
talkWikicode = talkWikicode.replace(/(\{\{WikiProject [^\|\}]+)(\}\})/gi, `$1|class=GA$2`);
return talkWikicode;
}
/**
 * Determine next |action= number in {{Article history}} template. This is so we can insert an action.
 * @private
 */
determineNextActionNumber(talkWikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let i = 1;
while ( true ) {
let regex = new RegExp(`\\|\\s*action${i}\\s*=`, 'i');
let hasAction = talkWikicode.match(regex);
if ( ! hasAction ) {
return i;
}
i++;
}
}
/**
 * @private
 */
updateArticleHistory(talkWikicode, topic, nominationPageTitle, listedOrFailed) {
if ( arguments.length !== 4 ) throw new Error('Incorrect # of arguments');
let nextActionNumber = this.determineNextActionNumber(talkWikicode);
if ( listedOrFailed !== 'listed' && listedOrFailed !== 'failed' ) {
throw new Error('InvalidArgumentException');
}
// always write our own topic. especially importnat for passing, because we want to use what the reviewer picked in the combo box, not what was already in the template.
talkWikicode = this.firstTemplateDeleteParameter(talkWikicode, 'Article ?history', 'topic');
let topicString = `\n|topic = ${topic}`;
// https://en.wikipedia.org/wiki/Template:Article_history#How_to_use_in_practice
let existingStatus = this.firstTemplateGetParameterValue(talkWikicode, 'Artricle history', 'currentstatus')
talkWikicode = this.firstTemplateDeleteParameter(talkWikicode, 'Article ?history', 'currentstatus');
let currentStatusString = this.getArticleHistoryNewStatus(existingStatus, listedOrFailed);
let addToArticleHistory = 
`|action${nextActionNumber} = GAN
|action${nextActionNumber}date = ~~~~~
|action${nextActionNumber}link = ${nominationPageTitle}
|action${nextActionNumber}result = ${listedOrFailed}`;
addToArticleHistory += currentStatusString + topicString;
talkWikicode = this.firstTemplateInsertCode(talkWikicode, 'Article ?history', addToArticleHistory);
return talkWikicode;
}
/**
 * @private
 */
getArticleHistoryNewStatus(existingStatus, listedOrFailed) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
if ( listedOrFailed === 'listed' ) {
switch ( existingStatus ) {
case 'FFA':
return '\n|currentstatus = FFA/GA';
case 'FFAC':
return '\n|currentstatus = FFAC/GA';
default:
return '\n|currentstatus = GA';
}
} else {
switch ( existingStatus ) {
case 'FFA':
return '\n|currentstatus = FFA';
case 'FFAC':
return '\n|currentstatus = FFAC';
case 'DGA':
return '\n|currentstatus = DGA';
default:
return '\n|currentstatus = FGAN';
}
}
}
/**
 * @private
 */
firstTemplateInsertCode(wikicode, templateNameRegExNoDelimiters, codeToInsert) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// TODO: handle nested templates
let regex = new RegExp(`(\\{\\{${templateNameRegExNoDelimiters}[^\\}]*)(\\}\\})`, 'i');
return wikicode.replace(regex, `$1\n${codeToInsert}\n$2`);
}
/**
 * @private
 */
firstTemplateGetParameterValue(wikicode, template, parameter) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// TODO: rewrite to be more robust. currently using a simple algorithm that is prone to failure
// new algorithm:
// find start of template. use regex /i (ignore case)
// iterate using loops until end of template found
// handle 
// handle triple {{{
// handle nested
let regex = new RegExp(`\\|\\s*${parameter}\\s*=\\s*([^\\n\\|\\}]*)\\s*`, '');
let result = wikicode.match(regex);
if ( wikicode.match(regex) === null ) return null;
return result[1];
}
/**
 * @param {RegExp} regex
 * @private
 */
preg_position(regex, haystack) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let matches = [...haystack.matchAll(regex)];
let hasMatches = matches.length;
if ( hasMatches ) {
return matches[0]['index'];
}
return false;
}
/**
 * @private
 */
findEndOfTemplate(wikicode, templateStartPosition) {
// TODO: handle triple braces, handle  tags
let nesting = 0;
let templateEndPosition = -1;
for ( let i = templateStartPosition + 1 /* +1 to skip the first {{, will throw off our nesting count */; i < wikicode.length; i++ ) {
let nextTwoChars = wikicode.slice(i, i + 2);
if ( nextTwoChars === '{{' ) {
nesting++;
continue;
} else if ( nextTwoChars === '}}' ) {
if ( nesting > 0 ) {
nesting--;
continue;
} else {
templateEndPosition = i + 2;
break;
}
}
}
return templateEndPosition;
}
/**
 * @private
 */
firstTemplateDeleteParameter(wikicode, templateRegEx, parameter) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// templateStartPosition
let regex = new RegExp('\{\{' + templateRegEx, 'gi');
let templateStartPosition = this.preg_position(regex, wikicode);
// templateEndPosition
let templateEndPosition = this.findEndOfTemplate(wikicode, templateStartPosition);
// slice
let firstPiece = wikicode.slice(0, templateStartPosition);
let secondPiece = wikicode.slice(templateStartPosition, templateEndPosition);
let thirdPiece = wikicode.slice(templateEndPosition);
// replace only inside the slice
let regex2 = new RegExp(`\\|\\s*${parameter}\\s*=\\s*([^\\n\\|\\}]*)\\s*`, '');
secondPiece = secondPiece.replace(regex2, '');
// glue back together
wikicode = firstPiece + secondPiece + thirdPiece;
return wikicode;
}
/**
 * @private
 */
removeFormattingThatInterferesWithSort(str) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return str.replace(/^[^\[]*\[\[(?:[^\|]+\|)?/, '') // delete anything in front of [[, [[, and anything inside the left half of a piped wikilink
.replace(/\]\][^\]]*$/, '') // delete ]], and anything after ]]
.replace(/"/g, '') // delete "
.replace(/''/g, '') // delete '' but not '
.replace(/^A /gi, '') // delete indefinite article "a"
.replace(/^An /gi, '') // delete indefinite article "an"
.replace(/^The /gi, '') // delete definite article "the"
}
/**
 * @private
 */
aSortsLowerThanB(a, b) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
// JavaScript appears to use an ASCII sort. See https://en.wikipedia.org/wiki/ASCII#Printable_characters
// make sure "A" and "a" sort the same. prevents a bug
a = a.toLowerCase();
b = b.toLowerCase();
a = this.removeDiacritics(a);
b = this.removeDiacritics(b);
let arr1 = [a, b];
let arr2 = [a, b];
// Sort numerically, not lexographically.
// Fixes a bug where the sort is 79, 8, 80 instead of 8, 79, 80
// Jon Wyatt, CC BY-SA 4.0, https://stackoverflow.com/a/44197285/3480193
let sortNumerically = (a, b) => a.localeCompare(b, 'en', { numeric: true });
return JSON.stringify(arr1.sort(sortNumerically)) === JSON.stringify(arr2);
}
/**
 * Jeroen Ooms, CC BY-SA 3.0, https://stackoverflow.com/a/18123985/3480193
 * @private
 */
removeDiacritics(str) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
var defaultDiacriticsRemovalMap = [
{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
{'base':'AA','letters':/[\uA732]/g},
{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
{'base':'AO','letters':/[\uA734]/g},
{'base':'AU','letters':/[\uA736]/g},
{'base':'AV','letters':/[\uA738\uA73A]/g},
{'base':'AY','letters':/[\uA73C]/g},
{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
{'base':'DZ','letters':/[\u01F1\u01C4]/g},
{'base':'Dz','letters':/[\u01F2\u01C5]/g},
{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
{'base':'LJ','letters':/[\u01C7]/g},
{'base':'Lj','letters':/[\u01C8]/g},
{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
{'base':'NJ','letters':/[\u01CA]/g},
{'base':'Nj','letters':/[\u01CB]/g},
{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
{'base':'OI','letters':/[\u01A2]/g},
{'base':'OO','letters':/[\uA74E]/g},
{'base':'OU','letters':/[\u0222]/g},
{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
{'base':'TZ','letters':/[\uA728]/g},
{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
{'base':'VY','letters':/[\uA760]/g},
{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
{'base':'aa','letters':/[\uA733]/g},
{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
{'base':'ao','letters':/[\uA735]/g},
{'base':'au','letters':/[\uA737]/g},
{'base':'av','letters':/[\uA739\uA73B]/g},
{'base':'ay','letters':/[\uA73D]/g},
{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
{'base':'dz','letters':/[\u01F3\u01C6]/g},
{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
{'base':'hv','letters':/[\u0195]/g},
{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
{'base':'lj','letters':/[\u01C9]/g},
{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
{'base':'nj','letters':/[\u01CC]/g},
{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
{'base':'oi','letters':/[\u01A3]/g},
{'base':'ou','letters':/[\u0223]/g},
{'base':'oo','letters':/[\uA74F]/g},
{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
{'base':'tz','letters':/[\uA729]/g},
{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
{'base':'vy','letters':/[\uA761]/g},
{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
];
for(var i=0; i<defaultDiacriticsRemovalMap.length; i++) {
str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base);
}
return str;
  }
/**
 * @private
 */
getGASubpageHeadingPosition(shortenedVersionInComboBox, wikicode) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
// chop off equals at beginning and end of line. we want to isolate a smaller piece to use as our needle.
let needle = /^={2,5}\s*(.*?)\s*={2,5}$/gm.exec(shortenedVersionInComboBox)[1];
// keep the === === equals signs surrounding the heading the same. to prevent matching the wrong heading when there are multiple headings with the same needle, e.g. ===Art=== and =====Art=====
let equalsSignsOnOneSide = /^(={2,5})/gm.exec(shortenedVersionInComboBox)[1];
// then insert this needle into a wide regex that includes equals, optional spaces next to the equals, and optional [[File:]]
let regex = new RegExp(`^${equalsSignsOnOneSide}\\s*(?:\\[\\[File:[^\\]]*\\]\\]\\s*)?${this.regExEscape(needle)}\\s*${equalsSignsOnOneSide}$`, 'gm');
let result = regex.exec(wikicode);
// if needle not found in haystack, return -1
if ( ! Array.isArray(result) ) return -1;
// else return location of first match
return result.index;
}
/**
 * @private
 */
findFirstStringAfterPosition(needle, haystack, position) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let len = haystack.length;
for ( let i = position; i < len; i++ ) {
let buffer = haystack.slice(i, len);
if ( buffer.startsWith(needle) ) {
return i;
}
}
return -1;
}
/**
 * CC BY-SA 4.0, jAndy, https://stackoverflow.com/a/4364902/3480193
 * @private
 */
insertStringIntoStringAtPosition(bigString, insertString, position) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
return [
bigString.slice(0, position),
insertString,
bigString.slice(position)
].join('');
}
/**
 * @private
 */
hasArticleHistoryTemplate(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return Boolean(wikicode.match(/\{\{Article ?history/i));
}
}
// === modules/GARCloserController.js ======================================================

class GARCloserController {
/**
 * @param {function} $ jQuery
 * @param {Object} mw mediawiki, https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw
 * @param {Location} location https://developer.mozilla.org/en-US/docs/Web/API/Window/location
 * @param {GARCloserWikicodeGenerator} wg
 * @param {GARCloserHTMLGenerator} hg
 */
async execute($, mw, location, wg, hg) {
if ( arguments.length !== 5 ) throw new Error('Incorrect # of arguments');
this.$ = $;
this.mw = mw;
this.location = location;
this.wg = wg;
this.hg = hg;
this.scriptLogTitle = `User:Novem Linguae/Scripts/GANReviewTool/GARLog`;
this.editSummarySuffix = ' ([[User:Novem Linguae/Scripts/GANReviewTool|GANReviewTool]])';
this.garPageTitle = this.mw.config.get('wgPageName'); // includes namespace, underscores instead of spaces
this.garPageTitle = this.garPageTitle.replace(/_/g, ' '); // underscores to spaces. prevents some bugs later
if ( ! this.shouldRunOnThisPageQuickChecks() ) {
return;
}
this.parentArticle = await this.confirmGARAndGetArticleName();
if ( ! this.parentArticle ) {
return;
}
this.talkPageTitle = `Talk:${this.parentArticle}`;
let hasGARLinkTemplate = await this.hasGARLinkTemplate(this.talkPageTitle);
let hasATOP = await this.hasATOP(this.garPageTitle);
if ( ! hasGARLinkTemplate || hasATOP ) {
return;
}
// place HTML on page
this.$('#mw-content-text').prepend(hg.getHTML())
this.$(`#GARCloser-Keep`).on('click', async () => {
await this.clickKeep();
});
this.$(`#GARCloser-Delist`).on('click', async () => {
await this.clickDelist();
});
}
/**
 * @private
 */
async clickKeep() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
// TODO: {{subst:GAR/result|result=outcome}} ~~~~ ? Ask Femke. May need to check if user already did it. Would do for both keep and delist.
try {
this.editSummary = `close GAR [[${this.garPageTitle}]] as keep` + this.editSummarySuffix;
this.deactivateBothButtons();
await this.processKeepForGARPage();
await this.processKeepForTalkPage();
if ( this.isCommunityAssessment() ) {
await this.makeCommunityAssessmentLogEntry();
}
} catch(err) {
this.error = err;
console.error(err);
}
await this.makeScriptLogEntry('keep');
if ( ! this.error ) {
this.pushStatus(`Done! Reloading...`);
location.reload();
} else {
this.pushStatus(`<span class="GARCloserTool-ErrorNotice">An error occurred :(</span>`);
}
}
/**
 * @private
 */
async clickDelist() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
try {
this.editSummary = `close GAR [[${this.garPageTitle}]] as delist` + this.editSummarySuffix;
this.deactivateBothButtons();
await this.processDelistForGARPage();
await this.processDelistForTalkPage();
await this.processDelistForArticle();
await this.processDelistForGAList();
if ( this.isCommunityAssessment() ) {
await this.makeCommunityAssessmentLogEntry();
}
} catch(err) {
this.error = err;
console.error(err);
}
await this.makeScriptLogEntry('delist');
if ( ! this.error ) {
this.pushStatus(`Done! Reloading...`);
location.reload();
} else {
this.pushStatus(`<span class="GARCloserTool-ErrorNotice">An error occurred :(</span>`);
}
}
/**
 * @private
 */
async hasGARLinkTemplate(title) {
let wikicode = await this.getWikicode(title);
return Boolean(wikicode.match(/\{\{GAR\/link/i));
}
/**
 * @private
 */
async hasATOP(title) {
let wikicode = await this.getWikicode(title);
return Boolean(wikicode.match(/\{\{Atop/i));
// TODO: don't match a small ATOP, must be ATOP of entire talk page
}
/**
 * @private
 */
deactivateBothButtons() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.$(`#GARCloser-Keep`).prop('disabled', true);
this.$(`#GARCloser-Delist`).prop('disabled', true);
}
/**
 * @private
 */
async processKeepForGARPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Place {{atop}} on GAR page. Replace {{GAR/current}} if present.`);
let wikicode = await this.getWikicode(this.garPageTitle);
let message = this.$(`#GARCloser-Message`).val();
wikicode = this.wg.processKeepForGARPage(wikicode, message, this.isCommunityAssessment());
this.garPageRevisionID = await this.makeEdit(this.garPageTitle, this.editSummary, wikicode);
if ( this.garPageRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
async processDelistForGARPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Place {{atop}} on GAR page`);
let wikicode = await this.getWikicode(this.garPageTitle);
let message = this.$(`#GARCloser-Message`).val();
wikicode = this.wg.processDelistForGARPage(wikicode, message, this.isCommunityAssessment());
this.garPageRevisionID = await this.makeEdit(this.garPageTitle, this.editSummary, wikicode);
if ( this.garPageRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
async processKeepForTalkPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Remove {{GAR/link}} from talk page, and update {{Article history}}`);
let wikicode = await this.getWikicode(this.talkPageTitle);
wikicode = this.wg.processKeepForTalkPage(wikicode, this.garPageTitle, this.talkPageTitle);
this.talkRevisionID = await this.makeEdit(this.talkPageTitle, this.editSummary, wikicode);
if ( this.talkRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
isCommunityAssessment() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
if ( this.garPageTitle.startsWith('Wikipedia:Good article reassessment/') ) {
return true;
}
return false;
}
/**
 * @private
 */
async makeCommunityAssessmentLogEntry() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Add entry to community assessment log`);
let archiveTitle = await this.getHighestNumberedPage("Wikipedia:Good article reassessment/Archive ");
// TODO: handle no existing log pages at all
let wikicode = await this.getWikicode(archiveTitle);
let garTemplateCount = this.countGARTemplates(wikicode);
let maximumNumberOfHeadingsAllowedInArchive = 82;
let newArchive = false;
if ( garTemplateCount >= maximumNumberOfHeadingsAllowedInArchive ) {
archiveTitle = this.incrementArchiveTitle(archiveTitle);
newArchive = true;
wikicode = ``;
this.incrementGARArchiveTemplate(archiveTitle);
}
let newWikicode = this.wg.makeCommunityAssessmentLogEntry(this.garPageTitle, wikicode, newArchive, archiveTitle);
this.garLogRevisionID = await this.makeEdit(archiveTitle, this.editSummary, newWikicode)
if ( this.garLogRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
async incrementGARArchiveTemplate(archiveTitle) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Update count at Template:GARarchive`);
let wikicode = await this.getWikicode('Template:GARarchive');
let newTemplateWikicode = this.wg.setGARArchiveTemplate(archiveTitle, wikicode);
this.garArchiveTemplateRevisionID = await this.makeEdit('Template:GARarchive', this.editSummary, newTemplateWikicode);
if ( this.garArchiveTemplateRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * Takes a Wikipedia page name with a number on the end, and returns that page name with the number on the end incremented by one. Example: "Wikipedia:Good article reassessment/Archive 67" -> "Wikipedia:Good article reassessment/Archive 68"
 * @private
 */
incrementArchiveTitle(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let number = title.match(/\d{1,}$/);
number++;
let titleWithNoNumber = title.replace(/\d{1,}$/, '');
return titleWithNoNumber + number.toString();
}
/**
 * Counts number of times "{{Wikipedia:Good article reassessment/" occurs in wikicode.
 * @private
 */
countGARTemplates(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return this.countOccurrencesInString(/\{\{Wikipedia:Good article reassessment\//g, wikicode);
}
/**
 * CC BY-SA 4.0, Lorenz Lo Sauer, https://stackoverflow.com/a/10671743/3480193
 * @param {RegExp} needleRegEx Make sure to set the /g parameter.
 * @private
 */
countOccurrencesInString(needleRegEx, haystack) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
return (haystack.match(needleRegEx)||[]).length;
}
/**
 * @param {'keep'|'delist'} keepOrDelist
 * @private
 */
async makeScriptLogEntry(keepOrDelist) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Add entry to GARCloser debug log`);
let username = this.mw.config.get('wgUserName');
let wikicode = this.wg.makeScriptLogEntryToAppend(
username,
keepOrDelist,
this.garPageTitle,
this.garPageRevisionID,
this.talkRevisionID,
this.articleRevisionID,
this.gaListRevisionID,
this.garLogRevisionID,
this.garArchiveTemplateRevisionID,
this.error
);
await this.appendToPage(this.scriptLogTitle, this.editSummary, wikicode);
}
/**
 * @private
 */
async processDelistForTalkPage() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Remove {{GAR/link}} from talk page, update {{Article history}}, remove |class=GA`);
let wikicode = await this.getWikicode(this.talkPageTitle);
this.gaListTitle = this.getGAListTitleFromTalkPageWikicode(wikicode);
wikicode = this.wg.processDelistForTalkPage(wikicode, this.garPageTitle, this.talkPageTitle);
this.talkRevisionID = await this.makeEdit(this.talkPageTitle, this.editSummary, wikicode);
if ( this.talkRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
getGAListTitleFromTalkPageWikicode(wikicode) {
let dictionary = {
'agriculture': 'Wikipedia:Good articles/Agriculture, food and drink',
'agriculture, food, and drink': 'Wikipedia:Good articles/Agriculture, food and drink',
'cuisine': 'Wikipedia:Good articles/Agriculture, food and drink',
'cuisines': 'Wikipedia:Good articles/Agriculture, food and drink',
'cultivation': 'Wikipedia:Good articles/Agriculture, food and drink',
'drink': 'Wikipedia:Good articles/Agriculture, food and drink',
'farming and cultivation': 'Wikipedia:Good articles/Agriculture, food and drink',
'farming': 'Wikipedia:Good articles/Agriculture, food and drink',
'food and drink': 'Wikipedia:Good articles/Agriculture, food and drink',
'food': 'Wikipedia:Good articles/Agriculture, food and drink',
'art': 'Wikipedia:Good articles/Art and architecture',
'architecture': 'Wikipedia:Good articles/Art and architecture',
'art and architecture': 'Wikipedia:Good articles/Art and architecture',
'engtech': 'Wikipedia:Good articles/Engineering and technology',
'applied sciences and technology': 'Wikipedia:Good articles/Engineering and technology',
'applied sciences': 'Wikipedia:Good articles/Engineering and technology',
'computers': 'Wikipedia:Good articles/Engineering and technology',
'computing and engineering': 'Wikipedia:Good articles/Engineering and technology',
'computing': 'Wikipedia:Good articles/Engineering and technology',
'eng': 'Wikipedia:Good articles/Engineering and technology',
'engineering': 'Wikipedia:Good articles/Engineering and technology',
'technology': 'Wikipedia:Good articles/Engineering and technology',
'transport': 'Wikipedia:Good articles/Engineering and technology',
'geography': 'Wikipedia:Good articles/Geography and places',
'geography and places': 'Wikipedia:Good articles/Geography and places',
'places': 'Wikipedia:Good articles/Geography and places',
'history': 'Wikipedia:Good articles/History',
'archaeology': 'Wikipedia:Good articles/History',
'heraldry': 'Wikipedia:Good articles/History',
'nobility': 'Wikipedia:Good articles/History',
'royalty': 'Wikipedia:Good articles/History',
'royalty, nobility and heraldry': 'Wikipedia:Good articles/History',
'world history': 'Wikipedia:Good articles/History',
'langlit': 'Wikipedia:Good articles/Language and literature',
'language and literature': 'Wikipedia:Good articles/Language and literature',
'languages and linguistics': 'Wikipedia:Good articles/Language and literature',
'languages and literature': 'Wikipedia:Good articles/Language and literature',
'languages': 'Wikipedia:Good articles/Language and literature',
'linguistics': 'Wikipedia:Good articles/Language and literature',
'lit': 'Wikipedia:Good articles/Language and literature',
'literature': 'Wikipedia:Good articles/Language and literature',
'math': 'Wikipedia:Good articles/Mathematics',
'mathematics and mathematicians': 'Wikipedia:Good articles/Mathematics',
'mathematics': 'Wikipedia:Good articles/Mathematics',
'maths': 'Wikipedia:Good articles/Mathematics',
'drama': 'Wikipedia:Good articles/Media and drama',
'ballet': 'Wikipedia:Good articles/Media and drama',
'dance': 'Wikipedia:Good articles/Media and drama',
'film': 'Wikipedia:Good articles/Media and drama',
'films': 'Wikipedia:Good articles/Media and drama',
'media and drama': 'Wikipedia:Good articles/Media and drama',
'media': 'Wikipedia:Good articles/Media and drama',
'opera': 'Wikipedia:Good articles/Media and drama',
'television': 'Wikipedia:Good articles/Media and drama',
'theater': 'Wikipedia:Good articles/Media and drama',
'theatre': 'Wikipedia:Good articles/Media and drama',
'theatre, film and drama': 'Wikipedia:Good articles/Media and drama',
'music': 'Wikipedia:Good articles/Music',
'albums': 'Wikipedia:Good articles/Music',
'classical compositions': 'Wikipedia:Good articles/Music',
'other music articles': 'Wikipedia:Good articles/Music',
'songs': 'Wikipedia:Good articles/Music',
'natsci': 'Wikipedia:Good articles/Natural sciences',
'astronomy': 'Wikipedia:Good articles/Natural sciences',
'astrophysics': 'Wikipedia:Good articles/Natural sciences',
'atmospheric science': 'Wikipedia:Good articles/Natural sciences',
'biology and medicine': 'Wikipedia:Good articles/Natural sciences',
'biology': 'Wikipedia:Good articles/Natural sciences',
'chemistry and materials science': 'Wikipedia:Good articles/Natural sciences',
'chemistry': 'Wikipedia:Good articles/Natural sciences',
'cosmology': 'Wikipedia:Good articles/Natural sciences',
'earth science': 'Wikipedia:Good articles/Natural sciences',
'earth sciences': 'Wikipedia:Good articles/Natural sciences',
'geology': 'Wikipedia:Good articles/Natural sciences',
'geophysics': 'Wikipedia:Good articles/Natural sciences',
'medicine': 'Wikipedia:Good articles/Natural sciences',
'meteorology and atmospheric sciences': 'Wikipedia:Good articles/Natural sciences',
'meteorology': 'Wikipedia:Good articles/Natural sciences',
'mineralogy': 'Wikipedia:Good articles/Natural sciences',
'natural science': 'Wikipedia:Good articles/Natural sciences',
'natural sciences': 'Wikipedia:Good articles/Natural sciences',
'physics and astronomy': 'Wikipedia:Good articles/Natural sciences',
'physics': 'Wikipedia:Good articles/Natural sciences',
'philrelig': 'Wikipedia:Good articles/Philosophy and religion',
'mysticism': 'Wikipedia:Good articles/Philosophy and religion',
'myth': 'Wikipedia:Good articles/Philosophy and religion',
'mythology': 'Wikipedia:Good articles/Philosophy and religion',
'phil': 'Wikipedia:Good articles/Philosophy and religion',
'philosophy and religion': 'Wikipedia:Good articles/Philosophy and religion',
'philosophy': 'Wikipedia:Good articles/Philosophy and religion',
'relig': 'Wikipedia:Good articles/Philosophy and religion',
'religion': 'Wikipedia:Good articles/Philosophy and religion',
'religion, mysticism and mythology': 'Wikipedia:Good articles/Philosophy and religion',
'socsci': 'Wikipedia:Good articles/Social sciences and society',
'business and economics': 'Wikipedia:Good articles/Social sciences and society',
'business': 'Wikipedia:Good articles/Social sciences and society',
'culture and society': 'Wikipedia:Good articles/Social sciences and society',
'culture': 'Wikipedia:Good articles/Social sciences and society',
'culture, society and psychology': 'Wikipedia:Good articles/Social sciences and society',
'economics and business': 'Wikipedia:Good articles/Social sciences and society',
'economics': 'Wikipedia:Good articles/Social sciences and society',
'education': 'Wikipedia:Good articles/Social sciences and society',
'gov': 'Wikipedia:Good articles/Social sciences and society',
'government': 'Wikipedia:Good articles/Social sciences and society',
'journalism and media': 'Wikipedia:Good articles/Social sciences and society',
'journalism': 'Wikipedia:Good articles/Social sciences and society',
'law': 'Wikipedia:Good articles/Social sciences and society',
'magazines and print journalism': 'Wikipedia:Good articles/Social sciences and society',
'media and journalism': 'Wikipedia:Good articles/Social sciences and society',
'politics and government': 'Wikipedia:Good articles/Social sciences and society',
'politics': 'Wikipedia:Good articles/Social sciences and society',
'psychology': 'Wikipedia:Good articles/Social sciences and society',
'social science': 'Wikipedia:Good articles/Social sciences and society',
'social sciences and society': 'Wikipedia:Good articles/Social sciences and society',
'social sciences': 'Wikipedia:Good articles/Social sciences and society',
'society': 'Wikipedia:Good articles/Social sciences and society',
'sports': 'Wikipedia:Good articles/Sports and recreation',
'everyday life': 'Wikipedia:Good articles/Sports and recreation',
'everydaylife': 'Wikipedia:Good articles/Sports and recreation',
'games': 'Wikipedia:Good articles/Sports and recreation',
'recreation': 'Wikipedia:Good articles/Sports and recreation',
'sport and recreation': 'Wikipedia:Good articles/Sports and recreation',
'sport': 'Wikipedia:Good articles/Sports and recreation',
'sports and recreation': 'Wikipedia:Good articles/Sports and recreation',
'video games': 'Wikipedia:Good articles/Video games',
'video and computer games': 'Wikipedia:Good articles/Video games',
'war': 'Wikipedia:Good articles/Warfare',
'aircraft': 'Wikipedia:Good articles/Warfare',
'battles and exercises': 'Wikipedia:Good articles/Warfare',
'battles': 'Wikipedia:Good articles/Warfare',
'decorations and memorials': 'Wikipedia:Good articles/Warfare',
'military': 'Wikipedia:Good articles/Warfare',
'military people': 'Wikipedia:Good articles/Warfare',
'units': 'Wikipedia:Good articles/Warfare',
'war and military': 'Wikipedia:Good articles/Warfare',
'warfare': 'Wikipedia:Good articles/Warfare',
'warships': 'Wikipedia:Good articles/Warfare',
'weapons and buildings': 'Wikipedia:Good articles/Warfare',
'weapons': 'Wikipedia:Good articles/Warfare',
}
let topic = wikicode.match(/\|\s*(?:sub)?topic\s*=\s*([^\|\}\n]+)/i)[1];
topic = topic.toLowerCase();
return dictionary[topic];
}
/**
 * @private
 */
async processDelistForArticle() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Remove {{Good article}} from article`);
let wikicode = await this.getWikicode(this.parentArticle);
wikicode = this.wg.processDelistForArticle(wikicode);
this.articleRevisionID = await this.makeEdit(this.parentArticle, this.editSummary, wikicode);
if ( this.articleRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * @private
 */
async processDelistForGAList() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
this.pushStatus(`Remove article from list of good articles`);
let wikicode = await this.getWikicode(this.gaListTitle);
wikicode = this.wg.processDelistForGAList(wikicode, this.parentArticle);
this.gaListRevisionID = await this.makeEdit(this.gaListTitle, this.editSummary, wikicode);
if ( this.gaListRevisionID === undefined ) {
throw new Error('Generated wikicode and page wikicode were identical, resulting in a null edit.');
}
}
/**
 * This also checks if GARCloser should run at all. A falsey result means that the supplied title is not a GAR page.
 * @private
 */
async confirmGARAndGetArticleName() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
let parentArticle = ``;
// CASE 1: INDIVIDUAL ==================================
let namespace = this.mw.config.get('wgNamespaceNumber');
let isTalkNamespace = ( namespace === 1 );
let isGASubPage = this.isGASubPage(this.garPageTitle);
let couldBeIndividualReassessment = isTalkNamespace && isGASubPage;
if ( couldBeIndividualReassessment ) {
parentArticle = this.getIndividualReassessmentParentArticle(this.garPageTitle);
let parentArticleWikicode = await this.getWikicode(`Talk:${parentArticle}`);
if ( parentArticleWikicode.match(/\{\{GAR\/link/i) ) {
return parentArticle;
}
}
// CASE 2: COMMUNITY ===================================
let couldBeCommunityReassessment = this.garPageTitle.startsWith('Wikipedia:Good article reassessment/');
if ( couldBeCommunityReassessment ) {
parentArticle = this.getCommunityReassessmentParentArticle(this.garPageTitle);
let parentArticleWikicode = await this.getWikicode(`Talk:${parentArticle}`);
if ( parentArticleWikicode.match(/\{\{GAR\/link/i) ) {
return parentArticle;
}
}
}
/**
 * @private
 */
getIndividualReassessmentParentArticle(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return title.match(/Talk:(.*)\/GA/)[1];
}
/**
 * @private
 */
getCommunityReassessmentParentArticle(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return title.match(/Wikipedia:Good article reassessment\/(.*)\/\d/)[1];
}
/**
 * @private
 */
async getWikicode(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "parse",
"page": title,
"prop": "wikitext",
"format": "json",
};
let result = await api.post(params);
if ( result['error'] ) return '';
let wikicode = result['parse']['wikitext']['*'];
return wikicode;
}
/**
 * @private
 */
async makeEdit(title, editSummary, wikicode) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "edit",
"format": "json",
"title": title,
"text": wikicode,
"summary": editSummary,
};
let result = await api.postWithToken('csrf', params);
let revisionID = result['edit']['newrevid'];
return revisionID;
}
/**
  * Lets you append without getting the Wikicode first. Saves an API query.
  * @private
  */
async appendToPage(title, editSummary, wikicodeToAppend) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let api = new this.mw.Api();
let params = {
"action": "edit",
"format": "json",
"title": title,
"appendtext": wikicodeToAppend,
"summary": editSummary,
};
let result = await api.postWithToken('csrf', params);
let revisionID = result['edit']['newrevid'];
return revisionID;
}
/**
  * Example: To get the latest archive of "Wikipedia:Good article reassessment/Archive ", use getHighestNumberedPage("Wikipedia:Good article reassessment/Archive "), which will return "Wikipedia:Good article reassessment/Archive 67"
  * @private
  */
async getHighestNumberedPage(prefix) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let t = new this.mw.Title(prefix); // https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Title
let prefixNoNamespace = t.getMainText();
let namespaceNumber = t.getNamespaceId();
let api = new this.mw.Api();
let params ={
"action": "query",
"format": "json",
"list": "allpages",
"apprefix": prefixNoNamespace,
"apnamespace": namespaceNumber,
"aplimit": "1",
"apdir": "descending"
};
let result = await api.post(params);
let title = result['query']['allpages'][0]['title'];
return title;
}
/**
  * @private
  */
pushStatus(statusToAdd) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
this.$(`#GARCloserTool-Status`).show();
this.$(`#GARCloserTool-Status > p`).append('<br />' + statusToAdd);
}
/**
 * @private
 */
shouldRunOnThisPageQuickChecks() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
// don't run when not viewing articles
let action = this.mw.config.get('wgAction');
if ( action != 'view' ) return false;
// don't run when viewing diffs
let isDiff = this.mw.config.get('wgDiffNewId');
if ( isDiff ) return false;
let isDeletedPage = ( ! this.mw.config.get('wgCurRevisionId') );
if ( isDeletedPage ) return false;
// always run in Novem's sandbox
if ( this.garPageTitle === 'User:Novem_Linguae/sandbox' ) return true;
return true;
}
/**
 * @private
 */
isGASubPage(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return Boolean(title.match(/\/GA\d{1,2}$/));
}
}
// === modules/GARCloserHTMLGenerator.js ======================================================
class GARCloserHTMLGenerator {
getHTML() {
if ( arguments.length !== 0 ) throw new Error('Incorrect # of arguments');
return `
<style>
#GARCloserTool {
border: 1px solid black;
padding: 1em;
margin-bottom: 1em;
}
#GARCloserTool h2 {
margin-top: 0;
}
#GARCloserTool strong {
text-decoration: underline;
}
#GARCloserTool p {
margin-top: 1.5em;
margin-bottom: 1.5em;
line-height: 1.5em;
}
#GARCloserTool-Status {
display: none;
}
.GARCloserTool-ErrorNotice {
color: red;
font-weight: bold;
}
#GARCloserTool textarea {
height: auto;
}
</style>
<div id="GARCloserTool">
<div id="GARCloserTool-Form">
<h2>
GAR Closer Tool
</h2>
<p>
<strong>Closing message</strong><br />
If you leave this blank, it will default to "Keep" or "Delist"
<textarea id="GARCloser-Message" rows="4"></textarea>
</p>
<p>
<button id="GARCloser-Keep">Keep</button>
<button id="GARCloser-Delist">Delist</button>
</p>
</div>
<div id="GARCloserTool-Status">
<p>
Processing...
</p>
</div>
</div>
`;
}
}
// === modules/GARCloserWikicodeGenerator.js ======================================================
class GARCloserWikicodeGenerator {
processKeepForGARPage(garPageWikicode, message, isCommunityAssessment) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
return this.processGARPage(garPageWikicode, message, isCommunityAssessment, 'Kept.', 'green');
}
processKeepForTalkPage(wikicode, garPageTitle, talkPageTitle) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
wikicode = this.removeTemplate('GAR/link', wikicode);
wikicode = this.convertGATemplateToArticleHistoryIfPresent(talkPageTitle, wikicode);
wikicode = this.updateArticleHistory('keep', wikicode, garPageTitle);
return wikicode;
}
makeCommunityAssessmentLogEntry(garTitle, wikicode, newArchive, archiveTitle) {
if ( arguments.length !== 4 ) throw new Error('Incorrect # of arguments');
let output = ``;
if ( newArchive ) {
let archiveNumber = this.getArchiveNumber(archiveTitle);
output +=
`{| class="messagebox"
|-
| [[Image:Filing cabinet icon.svg|50px|Archive]]
| This is an '''[[Wikipedia:How to archive a talk page|archive]]''' of past discussions. Its contents should be preserved in their current form. If you wish to start a new discussion or revive an old one, please do so on the <span class="plainlinks">[{{FULLURL:{{TALKSPACE}}:{{BASEPAGENAME}}}} current talk page]</span>.<!-- Template:Talkarchive -->
|}
{{Template:Process header green
 | title=  Good article reassessment
 | section  = (archive)
 | previous = ([[Wikipedia:Good article reassessment/Archive ${archiveNumber-1}|${archiveNumber-1}]])
 | next =([[Wikipedia:Good article reassessment/Archive ${archiveNumber+1}|Page ${archiveNumber+1}]]) 
 | shortcut =
 | notes=
}}
__TOC__`;
} else {
output += wikicode;
}
output += `\n{{${garTitle}}}`
return output;
}
setGARArchiveTemplate(newArchiveTitle, wikicode) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let archiveNumber = this.getArchiveNumber(newArchiveTitle);
return wikicode.replace(/^\d{1,}/, archiveNumber);
}
/**
 * @param {'keep'|'delist'} keepOrDelist
 */
makeScriptLogEntryToAppend(username, keepOrDelist, reviewTitle, garRevisionID, talkRevisionID, articleRevisionID, gaListRevisionID, garLogRevisionID, garArchiveTemplateRevisionID, error) {
if ( arguments.length !== 10 ) throw new Error('Incorrect # of arguments');
let textToAppend = `\n* `;
if ( error ) {
textToAppend += `<span style="color: red; font-weight: bold;">ERROR:</span> ${error}. `
}
let keepOrDelistPastTense = this.getKeepOrDelistPastTense(keepOrDelist);
textToAppend += `[[User:${username}|${username}]] ${keepOrDelistPastTense} [[${reviewTitle}]] at ~~~~~. `;
if ( garRevisionID ) {
textToAppend += `[[Special:Diff/${garRevisionID}|[Atop]]]`;
}
if ( garRevisionID ) {
textToAppend += `[[Special:Diff/${talkRevisionID}|[Talk]]]`;
}
if ( articleRevisionID ) {
textToAppend += `[[Special:Diff/${articleRevisionID}|[Article]]]`;
}
if ( gaListRevisionID ) {
textToAppend += `[[Special:Diff/${gaListRevisionID}|[List]]]`;
}
if ( garLogRevisionID ) {
textToAppend += `[[Special:Diff/${garLogRevisionID}|[Log]]]`;
}
if ( garArchiveTemplateRevisionID ) {
textToAppend += `[[Special:Diff/${garArchiveTemplateRevisionID}|[Tmpl]]]`;
}
return textToAppend;
}
processDelistForGARPage(garPageWikicode, message, isCommunityAssessment) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
return this.processGARPage(garPageWikicode, message, isCommunityAssessment, 'Delisted.', 'red');
}
processDelistForTalkPage(wikicode, garPageTitle, talkPageTitle) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
wikicode = this.removeTemplate('GAR/link', wikicode);
wikicode = this.convertGATemplateToArticleHistoryIfPresent(talkPageTitle, wikicode);
wikicode = this.updateArticleHistory('delist', wikicode, garPageTitle);
wikicode = this.removeGAStatusFromWikiprojectBanners(wikicode);
return wikicode;
}
processDelistForArticle(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
wikicode = wikicode.replace(/\{\{ga icon\}\}\n?/i, '');
wikicode = wikicode.replace(/\{\{ga article\}\}\n?/i, '');
wikicode = wikicode.replace(/\{\{good article\}\}\n?/i, '');
return wikicode;
}
processDelistForGAList(wikicode, articleToRemove) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let regex = new RegExp(`'{0,3}"?\\[\\[${this.regExEscape(articleToRemove)}.*\\]\\]"?'{0,3}\\n`);
wikicode = wikicode.replace(regex, '');
return wikicode;
}
/**
 * @private
 */
processGARPage(garPageWikicode, message, isCommunityAssessment, defaultText, atopColor) {
if ( arguments.length !== 5 ) throw new Error('Incorrect # of arguments');
message = this.setMessageIfEmpty(defaultText, message);
message = this.addSignatureIfMissing(message);
let messageForAtop = this.getMessageForAtop(isCommunityAssessment, message);
let result = this.placeATOP(garPageWikicode, messageForAtop, atopColor);
if ( isCommunityAssessment ) {
result = this.replaceGARCurrentWithGARResult(message, result);
}
return result;
}
/**
 * @private
 */
addSignatureIfMissing(message) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
if ( ! message.includes('~~~~') ) {
message += ' ~~~~';
}
return message;
}
/**
 * @private
 */
 setMessageIfEmpty(defaultText, message) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
if ( message === '' ) {
message = defaultText;
}
return message;
}
/**
 * @private
 */
getMessageForAtop(isCommunityAssessment, message) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let messageForAtop = message;
if ( isCommunityAssessment ) {
messageForAtop = '';
}
return messageForAtop;
}
/**
 * {{GAR/current}} and {{GAR/result}} are templates used in community reassessment GARs. The first needs to be swapped for the second when closing community reassessment GARs.
 * @private
 */
replaceGARCurrentWithGARResult(message, wikicode) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
message = message.replace(/ ?~~~~/g, '');
return wikicode.replace(/\{\{GAR\/current\}\}/i, `{{subst:GAR/result|result=${this.escapeTemplateParameter(message)}}} ~~~~`);
}
/**
 * @private
 */
escapeTemplateParameter(parameter) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
// TODO: This needs repair. Should only escape the below if they are not inside of a template. Should not escape them at all times. Commenting out for now.
// parameter = parameter.replace(/\|/g, '{{!}}');
// parameter = parameter.replace(/=/g, '{{=}}');
return parameter;
}
/**
 * Takes a Wikipedia page name with a number on the end, and returns that number.
 * @private
 */
getArchiveNumber(title) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return parseInt(title.match(/\d{1,}$/));
}
/**
 * @private
 */
placeATOP(wikicode, result, color) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let colorCode = '';
switch ( color ) {
case 'green':
colorCode = 'g';
break;
case 'red':
colorCode = 'r';
break;
}
// place top piece after first H2 or H3, if it exists
let resultText = result ? `\n| result = ${result}\n` : '';
let prependText =
`{{atop${colorCode}${resultText}}}`;
let hasH2OrH3 = wikicode.match(/^===?[^=]+===?$/m);
if ( hasH2OrH3 ) {
wikicode = wikicode.replace(/^(.*?===?[^=]+===?\n)(.*)$/s, '$1' + prependText + '\n$2');
} else {
wikicode = prependText + "\n" + wikicode;
}
// place bottom piece at end
let appendText = `{{abot}}`;
wikicode = wikicode.trim();
wikicode += `\n${appendText}\n`;
return wikicode;
}
/**
 * CC BY-SA 4.0, coolaj86, https://stackoverflow.com/a/6969486/3480193
 * @private
 */
regExEscape(string) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
 * @private
 */
removeTemplate(templateName, wikicode) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let regex = new RegExp(`\\{\\{${this.regExEscape(templateName)}[^\\}]*\\}\\}\\n`, 'i');
return wikicode.replace(regex, '');
}
/**
 * @private
 */
regexGetFirstMatchString(regex, haystack) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let matches = haystack.match(regex);
if ( matches !== null && matches[1] !== undefined ) {
return matches[1];
}
return null;
}
/**
 * There's a {{GA}} template that some people use instead of {{Article history}}. If this is present, replace it with {{Article history}}.
 * @private
 */
convertGATemplateToArticleHistoryIfPresent(talkPageTitle, wikicode) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let hasArticleHistory = Boolean(wikicode.match(/\{\{Article ? history([^\}]*)\}\}/gi));
let gaTemplateWikicode = this.regexGetFirstMatchString(/(\{\{GA[^\}]*\}\})/i, wikicode);
if ( ! hasArticleHistory && gaTemplateWikicode ) {
// delete {{ga}} template
wikicode = wikicode.replace(/\{\{GA[^\}]*\}\}\n?/i, '');
wikicode = wikicode.trim();
// parse its parameters
// example: |21:00, 12 March 2017 (UTC)|topic=Sports and recreation|page=1|oldid=769997774
let parameters = this.getParametersFromTemplateWikicode(gaTemplateWikicode);
// if no page specified, assume page is 1. so then the good article review link will be parsed as /GA1
let noPageSpecified = parameters['page'] === undefined;
if ( noPageSpecified ) {
parameters['page'] = 1;
}
let topicString = '';
if ( parameters['topic'] !== undefined ) {
topicString = `\n|topic = ${parameters['topic']}`;
} else if ( parameters['subtopic'] !== undefined ) { // subtopic is an alias only used in {{ga}}, it is not used in {{article history}}
topicString = `\n|topic = ${parameters['subtopic']}`;
}
let oldIDString = '';
if ( parameters['oldid'] !== undefined ) {
oldIDString = `\n|action1oldid = ${parameters['oldid']}`;
}
// insert {{article history}} template
let addToTalkPageAboveWikiProjects = 
`{{Article history
|currentstatus = GA${topicString}
|action1 = GAN
|action1date = ${parameters[1]}
|action1link = ${talkPageTitle}/GA${parameters['page']}
|action1result = listed${oldIDString}
}}`;
wikicode = this.addToTalkPageAboveWikiProjects(wikicode, addToTalkPageAboveWikiProjects);
}
return wikicode;
}
/**
 * Adds wikicode right above {{WikiProject X}} or {{WikiProject Banner Shell}} if present, or first ==Header== if present, or at bottom of page. Treat {{Talk:abc/GA1}} as a header.
 * @private
 */
addToTalkPageAboveWikiProjects(talkPageWikicode, wikicodeToAdd) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
if ( ! talkPageWikicode ) {
return wikicodeToAdd;
}
// Find first WikiProject or WikiProject banner shell template
let wikiProjectLocation = false;
let dictionary = ['wikiproject', 'wpb', 'wpbs', 'wpbannershell', 'wp banner shell', 'bannershell', 'scope shell', 'project shell', 'multiple wikiprojects', 'football'];
for ( let value of dictionary ) {
let location = talkPageWikicode.toUpperCase().indexOf('{{' + value.toUpperCase()); // case insensitive
if ( location !== -1 ) {
// if this location is higher up than the previous found location, overwrite it
if ( wikiProjectLocation === false || wikiProjectLocation > location ) {
wikiProjectLocation = location;
}
}
}
// Find first heading
let headingLocation = talkPageWikicode.indexOf('==');
// Find first {{Talk:abc/GA1}} template
let gaTemplateLocation = this.preg_position(new RegExp(`\\{\\{[^\\}]*\\/GA\\d{1,2}\\}\\}`, 'gis'), talkPageWikicode);
// Set insert location
let insertPosition;
if ( wikiProjectLocation !== false ) {
insertPosition = wikiProjectLocation;
} else if ( headingLocation !== -1 ) {
insertPosition = headingLocation;
} else if ( gaTemplateLocation !== false ) {
insertPosition = gaTemplateLocation;
} else {
insertPosition = talkPageWikicode.length; // insert at end of page
}
// if there's a {{Talk:abc/GA1}} above a heading, adjust for this
if (
headingLocation !== -1 &&
gaTemplateLocation !== false &&
gaTemplateLocation < insertPosition
) {
insertPosition = gaTemplateLocation;
}
// If there's excess newlines in front of the insert location, delete the newlines
let deleteTopPosition = false;
let deleteBottomPosition = false;
let pos = insertPosition <= 0 ? 0 : insertPosition - 1;
let i = 1;
while ( pos != 0 ) {
let char = talkPageWikicode.substr(pos, 1);
if ( char == "\n" ) {
if ( i != 1 && i != 2 ) { // skip first two \n, those are OK to keep
deleteTopPosition = pos;
if ( i == 3 ) {
deleteBottomPosition = insertPosition;
}
}
insertPosition = pos; // insert position should back up past all \n's.
i++;
pos--;
} else {
break;
}
}
if ( deleteTopPosition !== false ) {
talkPageWikicode = this.deleteMiddleOfString(talkPageWikicode, deleteTopPosition, deleteBottomPosition);
}
let lengthOfRightHalf = talkPageWikicode.length - insertPosition;
let leftHalf = talkPageWikicode.substr(0, insertPosition);
let rightHalf = talkPageWikicode.substr(insertPosition, lengthOfRightHalf);
if ( insertPosition == 0 ) {
return wikicodeToAdd + "\n" + talkPageWikicode;
} else {
return leftHalf + "\n" + wikicodeToAdd + rightHalf;
}
}
/**
 * @param {RegExp} regex
 * @private
 */
preg_position(regex, haystack) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let matches = [...haystack.matchAll(regex)];
let hasMatches = matches.length;
if ( hasMatches ) {
return matches[0]['index'];
}
return false;
}
/**
 * @private
 */
deleteMiddleOfString(string, deleteStartPosition, deleteEndPosition) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let part1 = string.substr(0, deleteStartPosition);
let part2 = string.substr(deleteEndPosition);
let final_str = part1 + part2;
return final_str;
}
/**
 * @returns {Object} Parameters, with keys being equivalent to the template parameter names. Unnamed parameters will be 1, 2, 3, etc.
 * @private
 */
getParametersFromTemplateWikicode(wikicodeOfSingleTemplate) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
wikicodeOfSingleTemplate = wikicodeOfSingleTemplate.slice(2, -2); // remove {{ and }}
// TODO: explode without exploding | inside of inner templates
let strings = wikicodeOfSingleTemplate.split('|');
let parameters = {};
let unnamedParameterCount = 1;
let i = 0;
for ( let string of strings ) {
i++;
if ( i == 1 ) {
continue; // skip the template name, this is not a parameter 
}
let hasEquals = string.indexOf('=');
if ( hasEquals === -1 ) {
parameters[unnamedParameterCount] = string;
unnamedParameterCount++;
} else {
let matches = string.match(/^([^=]*)=(.*)/s); // isolate param name and param value by looking for first equals sign
let paramName = matches[1].trim().toLowerCase(); 
let paramValue = matches[2].trim();
parameters[paramName] = paramValue;
}
}
return parameters;
}
/**
 * @param {'keep'|'delist'} keepOrDelist
 * @private
 */
updateArticleHistory(keepOrDelist, wikicode, garPageTitle) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
let nextActionNumber = this.determineNextActionNumber(wikicode);
if ( keepOrDelist !== 'keep' && keepOrDelist !== 'delist' ) {
throw new Error('InvalidArgumentException');
}
let topic = this.firstTemplateGetParameterValue(wikicode, 'Artricle history', 'topic');
let topicString = '';
if ( ! topic ) {
topicString = `\n|topic = ${topic}`;
}
// https://en.wikipedia.org/wiki/Template:Article_history#How_to_use_in_practice
let existingStatus = this.firstTemplateGetParameterValue(wikicode, 'Artricle history', 'currentstatus')
wikicode = this.firstTemplateDeleteParameter(wikicode, 'Article history', 'currentstatus');
let currentStatusString = this.getArticleHistoryNewStatus(existingStatus, keepOrDelist);
let result = this.getKeepOrDelistPastTense(keepOrDelist);
let addToArticleHistory = 
`|action${nextActionNumber} = GAR
|action${nextActionNumber}date = ~~~~~
|action${nextActionNumber}link = ${garPageTitle}
|action${nextActionNumber}result = ${result}`;
addToArticleHistory += currentStatusString + topicString;
wikicode = this.firstTemplateInsertCode(wikicode, ['Article history', 'ArticleHistory'], addToArticleHistory);
return wikicode;
}
/**
 * @private
 */
getKeepOrDelistPastTense(keepOrDelist) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
switch ( keepOrDelist ) {
case 'keep':
return 'kept';
case 'delist':
return 'delisted';
}
}
/**
 * Determine next |action= number in {{Article history}} template. This is so we can insert an action.
 * @private
 */
determineNextActionNumber(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
let i = 1;
while ( true ) {
let regex = new RegExp(`\\|\\s*action${i}\\s*=`, 'i');
let hasAction = wikicode.match(regex);
if ( ! hasAction ) {
return i;
}
i++;
}
}
/**
 * @private
 */
firstTemplateGetParameterValue(wikicode, template, parameter) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// TODO: rewrite to be more robust. currently using a simple algorithm that is prone to failure
// new algorithm:
// find start of template. use regex /i (ignore case)
// iterate using loops until end of template found
// handle 
// handle triple {{{
// handle nested
let regex = new RegExp(`\\|\\s*${parameter}\\s*=\\s*([^\\n\\|\\}]*)\\s*`, '');
let result = wikicode.match(regex);
if ( wikicode.match(regex) === null ) return null;
return result[1];
}
/**
 * @private
 */
getArticleHistoryNewStatus(existingStatus, keepOrDelist) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
if ( keepOrDelist === 'keep' ) {
return `\n|currentstatus = ${existingStatus}`;
} else {
return '\n|currentstatus = DGA';
}
}
/**
 * @private
 * @param {Array} templateNameArrayCaseInsensitive
 */
firstTemplateInsertCode(wikicode, templateNameArrayCaseInsensitive, codeToInsert) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
for ( let templateName of templateNameArrayCaseInsensitive ) {
let strPosOfEndOfFirstTemplate = this.getStrPosOfEndOfFirstTemplateFound(wikicode, templateName);
if ( strPosOfEndOfFirstTemplate !== null ) {
let insertPosition = strPosOfEndOfFirstTemplate - 2; // 2 characters from the end, right before }}
let result = this.insertStringIntoStringAtPosition(wikicode, `\n${codeToInsert}\n`, insertPosition);
return result;
}
}
}
/**
 * CC BY-SA 4.0, jAndy, https://stackoverflow.com/a/4364902/3480193
 * @private
 */
insertStringIntoStringAtPosition(bigString, insertString, position) {
return [
bigString.slice(0, position),
insertString,
bigString.slice(position)
].join('');
}
/**
 * Grabs string position of the END of first {{template}} contained in wikicode. Case insensitive. Returns null if no template found. Handles nested templates.
 * @private
 * @returns {int|null}
 */
getStrPosOfEndOfFirstTemplateFound(wikicode, templateName) {
if ( arguments.length !== 2 ) throw new Error('Incorrect # of arguments');
let starting_position = wikicode.toLowerCase().indexOf("{{" + templateName.toLowerCase());
if ( starting_position === -1 ) return null;
let counter = 0;
let length = wikicode.length;
for ( let i = starting_position + 2; i < length; i++ ) {
let next_two = wikicode.substr(i, 2);
if ( next_two == "{{" ) {
counter++;
continue;
} else if ( next_two == "}}" ) {
if ( counter == 0 ) {
return i + 2; // +2 to account for next_two being }} (2 characters)
} else {
counter--;
continue;
}
}
}
return null;
}
/**
 * @private
 */
removeGAStatusFromWikiprojectBanners(wikicode) {
if ( arguments.length !== 1 ) throw new Error('Incorrect # of arguments');
return wikicode.replace(/(\|\s*class\s*=\s*)([^\}\|\s]*)/gi, '$1');
}
/**
 * @private
 */
firstTemplateDeleteParameter(wikicode, template, parameter) {
if ( arguments.length !== 3 ) throw new Error('Incorrect # of arguments');
// TODO: rewrite to be more robust. currently using a simple algorithm that is prone to failure
let regex = new RegExp(`\\|\\s*${parameter}\\s*=\\s*([^\\n\\|\\}]*)\\s*`, '');
return wikicode.replace(regex, '');
}
}
});
// </nowiki>

/* Novem Linguae/Scripts/UserRightsDiff.js */
// <nowiki>
/*
A typical user rights log entry might look like this:
11:29, August 24, 2021 ExampleUser1 talk contribs changed group membership for ExampleUser2 from edit filter helper, autopatrolled, extended confirmed user, page mover, new page reviewer, pending changes reviewer, rollbacker and template editor to autopatrolled, extended confirmed user, pending changes reviewer and rollbacker (inactive 1+ years. should you return and require access again please see WP:PERM) (thank)
What the heck perms were removed? Hard to tell right? This user script adds a "DIFF" of the perms that were added or removed, on its own line, and highlights it green for added, yellow for removed.
[ADDED template editor] [REMOVED edit filter helper, patroller]
This script works in Special:UserRights, in Watchlists, and when clicking "rights" in the user script User:BradV/Scripts/SuperLinks.js
*/
$(function() {
/** Don't delete "(none)". Delete all other parentheses and tags. */
function deleteParenthesesAndTags(text) {
// delete all U+200E. this character shows up on watchlists, and causes an extra space if not deleted.
text = text.replace("\u200E", '');
// get rid of 2 layers of nested parentheses. will help with some edge cases.
text = text.replace(/\([^()]*\([^()]*\)[^()]*\)/gs, '');
// get rid of 1 layer of nested parentheses, except for (none)
text = text.replace(/(?!\(none\))\(.*?\){1,}/gs, '');
// remove Tag: and anything after it
text = text.replace(/ Tags?:.*?$/, '');
// cleanup so it's easier to write unit tests (output doesn't have extra spaces in it)
text = text.replace(/ {2,}/gs, ' ');
text = text.replace(/(\S) ,/gs, '$1,');
text = text.trim();
return text;
}
/** Helpful for preventing issues where the username contains the word "from", which is searched for by a RegEx later. */
function deleteBeginningOfLogEntry(text) {
text = text.replace(/^.*changed group membership for .* from /, ' from ');
text = text.replace(/^.*was automatically updated from /gs, ' from ');
return text;
}
function permStringToArray(string) {
string = string.replace(/^(.*) and (.*?$)/, '$1, $2');
if ( string === '(none)') {
return [];
}
let array = string.split(', ').map(function(str) {
str = str.trim();
str = str.replace(/[\s.,\/#!$%\^&\*;:{}=\-_`~()]{2,}/g, ''); // remove fragments of punctuation. can result when trying to delete nested parentheses. will delete fragments such as " .)"
return str;
});
return array;
}
function permArrayToString(array) {
array = array.join(', ');
return array;
}
function checkLine() {
let text = $(this).text();
let from, to;
try {
text = deleteParenthesesAndTags(text);
text = deleteBeginningOfLogEntry(text);
let matches = / from (.*?) to (.*?)(?: \(.*)?$/.exec(text);
from = permStringToArray(matches[1]);
to = permStringToArray(matches[2]);
} catch(err) {
throw new Error("UserRightsDiff.js error. Error was: " + err + ". Input text was: " + $(this).text());
}
let added = to.filter(x => !from.includes(x));
let removed = from.filter(x => !to.includes(x));
added = added.length > 0 ?
'<span class="user-rights-diff" style="background-color:lawngreen">[ADDED: ' + permArrayToString(added) + ']</span>' :
'';
removed = removed.length > 0 ?
'<span class="user-rights-diff" style="background-color:yellow">[REMOVED: ' + permArrayToString(removed) + ']</span>' :
'';
let noChange = added.length === 0 && removed.length === 0 ?
'<span class="user-rights-diff" style="background-color:lightgray">[NO CHANGE]</span>' :
'';
$(this).append(`<br />${added} ${removed} ${noChange}`);
}
function checkLog() {
$('body').off('DOMNodeInserted'); // prevent infinite loop
if ( $('.user-rights-diff').length === 0 ) { // don't run twice on the same page
$('.mw-logevent-loglines .mw-logline-rights').each( checkLine ); // Special:UserRights, BradV SuperLinks
$('.mw-changeslist-log-rights .mw-changeslist-line-inner').each( checkLine ); // watchlist
}
$('body').on('DOMNodeInserted', '.mw-logevent-loglines', checkLog);;
};
// User:BradV/Scripts/SuperLinks.js
$('body').on('DOMNodeInserted', '.mw-logevent-loglines', checkLog);
// Special:UserRights
checkLog();
});
// </nowiki>

/* Enterprisey/AFCRHS.js */
//<nowiki>
(function() {
if (mw.config.get("wgPageName").indexOf('Wikipedia:Articles_for_creation/Redirects_and_categories') === -1) {
return;
}
var afcHelper_RedirectPageName = mw.config.get("wgPageName").replace(/_/g, ' ');
var afcHelper_RedirectSubmissions = new Array();
var afcHelper_RedirectSections = new Array();
var afcHelper_advert = ' ([[User:Enterprisey/AFCRHS|AFCRHS.js]])';
var afcHelper_numTotal = 0;
var afcHelper_AJAXnumber = 0;
var afcHelper_Submissions = new Array();
var needsupdate = new Array();
var afcHelper_redirectDecline_reasonhash = {
'exists': 'The title you suggested already exists on Wikipedia',
'blank': 'We cannot accept empty submissions',
'notarget': ' A redirect cannot be created unless the target is an existing article. Either you have not specified the target, or the target does not exist',
'unlikely': 'The title you suggested seems unlikely. Could you provide a source showing that it is a commonly used alternate name?',
'notredirect': 'This request is not a redirect request',
'custom': ''
};
var afcHelper_categoryDecline_reasonhash = {
'exists': 'The category you suggested already exists on Wikipedia',
'blank': 'We cannot accept empty submissions',
'unlikely': 'It seems unlikely that there are enough pages to support this category',
'notcategory': 'This request is not a category request',
'custom': ''
};
function afcHelper_redirect_init() {
pagetext = afcHelper_getPageText(afcHelper_RedirectPageName);
// cleanup the wikipedia links for preventing stuff like https://en.wikipedia.org/w/index.php?diff=576244067&oldid=576221437
pagetext = afcHelper_cleanuplinks(pagetext);
// first, strip out the parts before the first section.
var section_re = /==.*?==/;
pagetext = pagetext.substring(pagetext.search(section_re));
// then split it into the rest of the sections
afcHelper_RedirectSections = pagetext.match(/^==.*?==$((\r?\n?)(?!==[^=]).*)*/img);
// parse the sections.
for (var i = 0; i < afcHelper_RedirectSections.length; i++) {
var closed = /(\{\{\s*afc(?!\s+comment)|This is an archived discussion)/i.test(afcHelper_RedirectSections[i]);
if (!closed) {
// parse.
var header = afcHelper_RedirectSections[i].match(section_re)[0];
if (header.search(/Redirect request/i) !== -1) {
var wikilink_re = /\[\[(\s*[^=]*?)*?\]\]/g;
var links = header.match(wikilink_re);
if (!links) continue;
for (var l = 0; l < links.length; l++) {
links[l] = links[l].replace(/[\[\]]/g, '');
if (links[l].charAt(0) === ':') links[l] = links[l].substring(1);
}
var re = /Target of redirect:\s*\[\[([^\[\]]*)\]\]/i;
re.test(afcHelper_RedirectSections[i]);
var to = $.trim(RegExp.$1);
var reasonRe = /Reason:[ \t]*?(.+)/i;
var reasonMatch = reasonRe.exec(afcHelper_RedirectSections[i]);
var reason = reasonMatch && reasonMatch[1].trim() ? reasonMatch[1] : null;
var sourceRe = /Source.*?:[ \t]*?(.+)/i;
var sourceMatch = sourceRe.exec(afcHelper_RedirectSections[i]);
var source = sourceMatch && sourceMatch[1].trim() ? sourceMatch[1] : null;
var submission = {
type: 'redirect',
from: new Array(),
section: i,
to: to,
title: to,
reason: reason,
source: source
};
for (var j = 0; j < links.length; j++) {
var sub = {
type: 'redirect',
to: to,
id: afcHelper_numTotal,
title: links[j],
action: ''
};
submission.from.push(sub);
afcHelper_Submissions.push(sub);
afcHelper_numTotal++;
}
afcHelper_RedirectSubmissions.push(submission);
} else if (header.search(/Category request/i) !== -1) {
// Find a wikilink in the header, and assume it's the category to create
var category_name = /\[\[[^\[\]]+\]\]/.exec(header);
if (!category_name) continue;
category_name = category_name[0];
category_name = category_name.replace(/[\[\]]/g, '');
category_name = category_name.replace(/Category\s*:\s*/gi, 'Category:');
if (category_name.charAt(0) === ':') category_name = category_name.substring(1);
// Figure out the parent categories
var request_text = afcHelper_RedirectSections[i].substring(header.length);
// We only want categories listed under the "Parent category/categories" heading,
// *NOT* any categories listed under "Example pages which belong to this category".
var idx_of_parent_heading = request_text.indexOf('Parent category/categories');
if (idx_of_parent_heading >= 0 ) {
request_text = request_text.substring(idx_of_parent_heading);
}
var parent_cats = [];
var parent_cat_match = null;
var parent_cat_regex = /\[\[\s*:\s*(Category:[^\]\[]*)\]\]/ig;
do {
parent_cat_match = parent_cat_regex.exec(request_text);
if (parent_cat_match) {
parent_cats.push(parent_cat_match[1]);
}
} while (parent_cat_match);
var submission = {
type: 'category',
title: category_name,
section: i,
id: afcHelper_numTotal,
action: '',
parents: parent_cats.join(',')
};
afcHelper_numTotal++;
afcHelper_RedirectSubmissions.push(submission);
afcHelper_Submissions.push(submission);
}
} // end if (!closed)
} // end loop over sections
// Build the form
var $form = $('<h3>Reviewing AfC redirect requests</h3>');
displayMessage($form);
var $messageDiv = $form.parent();
// now layout the text.
var afcHelper_Redirect_empty = 1;
var ACTIONS = [
{label: 'Accept', value: 'accept'},
{label: 'Decline', value: 'decline'},
{label: 'Comment',value: 'comment'},
{label: 'None', selected: true, value: 'none'}
];
for (var k = 0; k < afcHelper_RedirectSubmissions.length; k++) {
if (afcHelper_RedirectSubmissions[k].to !== undefined)
var submissionname = afcHelper_RedirectSubmissions[k].to.replace(/\s/g,'');
else
var submissionname = "";
var $thisSubList = $('<ul>');
var $thisSubListElement = $('<li>');
if (afcHelper_RedirectSubmissions[k].type === 'redirect') {
$thisSubListElement.append('Redirect(s) to ');
if (!submissionname) {
for (var i = afcHelper_RedirectSubmissions[k].from.length - 1; i >= 0; i--) {
needsupdate.push({
id: afcHelper_RedirectSubmissions[k].from[i].id,
reason: 'notarget'
});
}
} else if (!afcHelper_RedirectSubmissions[k].to) {
for (var i = afcHelper_RedirectSubmissions[k].from.length - 1; i >= 0; i--) {
needsupdate.push({
id: afcHelper_RedirectSubmissions[k].from[i].id,
reason: 'notredirect'
});
}
}
if (afcHelper_RedirectSubmissions[k] === '' || afcHelper_RedirectSubmissions[k] === ' ') {
$thisSubListElement.append('Empty submission \#' + afcHelper_Redirect_empty);
afcHelper_Redirect_empty++;
} else {
if (submissionname.length > 0) {
$thisSubListElement.append($('<a>')
.attr('href', mw.config.get("wgArticlePath").replace("$1", encodeURIComponent(afcHelper_RedirectSubmissions[k].to)))
.attr('target', '_blank')
.text(afcHelper_RedirectSubmissions[k].to));
} else {
$thisSubListElement.append('<b>no target given</b>: ');
}
}
var $fromList = $('<ul>').appendTo($thisSubListElement);
for (var l = 0; l < afcHelper_RedirectSubmissions[k].from.length; l++) {
var from = afcHelper_RedirectSubmissions[k].from[l];
var toarticle = from.title;
if (toarticle.replace(/\s*/gi, "").length == 0) toarticle = "<b>no title specified</b>, check the request details";
var reasonAndSource = $('<ul>');
if(afcHelper_RedirectSubmissions[k].reason)
reasonAndSource.append('<li>Reason: ' + afcHelper_RedirectSubmissions[k].reason + '</li>');
if(afcHelper_RedirectSubmissions[k].source)
reasonAndSource.append('<li>Source: ' + afcHelper_RedirectSubmissions[k].source + '</li>');
var googleSearchUrl = 'http://www.google.com/search?q="' +
encodeURIComponent(toarticle) + '"+-wikipedia.org';
$fromList.append($('<li>')
.append('From: ' + toarticle + ' (<small><a href=\'' + googleSearchUrl + '\'" target="_blank">' +
'Google</a> <b>&middot;</b> <a href="https://en.wikipedia.org/wiki/Special:WhatLinksHere/' +
encodeURIComponent(toarticle) + '" target="_blank">what links here</a>)</small><br/>')
.append(reasonAndSource)
.append($('<label>').attr('for', 'afcHelper_redirect_action_' + from.id).text('Action: '))
.append(afcHelper_generateSelectObject('afcHelper_redirect_action_' + from.id, ACTIONS,
afcHelper_redirect_makeActionChange(from.id)))
.append($('<div>').attr('id', 'afcHelper_redirect_extra_' + from.id)));
}
} else {
var subId = afcHelper_RedirectSubmissions[k].id;
$thisSubListElement.append('Category submission: ')
.append($('<a>')
.attr('href', '/wiki/' + afcHelper_RedirectSubmissions[k].title)
.attr('title', afcHelper_RedirectSubmissions[k].title)
.text(afcHelper_RedirectSubmissions[k].title))
.append('<br />')
.append($('<label>').attr('for', 'afcHelper_redirect_action_' + subId).text('Action: '))
.append(afcHelper_generateSelectObject('afcHelper_redirect_action_' + subId, ACTIONS,
afcHelper_redirect_makeActionChange(subId)))
.append($('<div>').attr('id', 'afcHelper_redirect_extra_' + subId));
}
$thisSubList.append($thisSubListElement);
$messageDiv.append($thisSubList);
} // end loop over sections
$messageDiv.append($('<button>')
.attr('id', 'afcHelper_redirect_done_button')
.attr('name', 'afcHelper_redirect_done_button')
.text('Done')
.click(afcHelper_redirect_performActions));
for (var y = 0; y < needsupdate.length; y++){
$('#afcHelper_redirect_action_'+needsupdate[y].id).attr('value', 'decline');
afcHelper_redirect_onActionChange(needsupdate[y].id);
$('#afcHelper_redirect_decline_'+needsupdate[y].id).attr('value', needsupdate[y].reason);
}
}
function afcHelper_redirect_makeActionChange(id) {
return function() { afcHelper_redirect_onActionChange(id); };
}
function afcHelper_redirect_onActionChange(id) {
console.log("Entering afcHelper_redirect_onActionChange, id = " + id);
var $extra = $("#afcHelper_redirect_extra_" + id);
var selectValue = $("#afcHelper_redirect_action_" + id).val();
$extra.html(''); // Blank it first
if (selectValue === 'accept') {
if (afcHelper_Submissions[id].type === 'redirect') {
$extra.append('<label for="afcHelper_redirect_from_' + id + '">From: </label>');
$extra.append($('<input>')
.attr('type', 'text')
.attr('name', 'afcHelper_redirect_from_' + id)
.attr('id', 'afcHelper_redirect_from_' + id)
.attr('value', afcHelper_escapeHtmlChars(afcHelper_Submissions[id].title)));
$extra.html($extra.html() + '&nbsp;<br /><label for="afcHelper_redirect_to_' + id + '">To: </label><input type="text" ' + 'name="afcHelper_redirect_to_' + id + '" id="afcHelper_redirect_to_' + id + '" value="' + afcHelper_escapeHtmlChars(afcHelper_Submissions[id].to) + '" />');
$extra.html($extra.html() + '<br /><label for="afcHelper_redirect_append_' + id + '">Template to append: (<a href="https://en.wikipedia.org/wiki/Wikipedia:TMR" target="_blank">Help</a>)</label>');
$extra.html($extra.html() + afcHelper_generateSelect('afcHelper_redirect_append_' + id, [
{
label: 'None',
selected: true,
value: 'none'
}, {
labelAndValue: 'Frequently used',
disabled: true
},
{ labelAndValue: 'R from alternative language' },
{ labelAndValue: 'R from alternative name' },
{ labelAndValue: 'R from modification' },
{ labelAndValue: 'R to section' },
{ labelAndValue: 'R from diacritic' },
{ labelAndValue: 'R to diacritic' },
{
labelAndValue: 'From – abbreviation, capitalisation, and grammar',
disabled: true
},
{ labelAndValue: 'R from acronym' },
{ labelAndValue: 'R from initialism' },
{ labelAndValue: 'R from CamelCase' },
{ labelAndValue: 'R from miscapitalisation' },
{ labelAndValue: 'R from other capitalisation' },
{ labelAndValue: 'R from modification' },
{ labelAndValue: 'R from plural' },
{
label: 'From parts of speach',
value: 'From parts of speach',
disabled: true
},
{ labelAndValue: 'R from adjective' },
{ labelAndValue: 'R from adverb' },
{ labelAndValue: 'R from common noun' },
{ labelAndValue: 'R from gerund' },
{ labelAndValue: 'R from proper noun' },
{ labelAndValue: 'R from verb' },
{
labelAndValue: 'From – spelling',
disabled: true
},
{ labelAndValue: 'R from alternative spelling' },
{ labelAndValue: 'R from misspelling' },
{ labelAndValue: 'R from American English' },
{ labelAndValue: 'R from British English' },
{ labelAndValue: 'R from ASCII-only' },
{ labelAndValue: 'R from diacritic' },
{ labelAndValue: 'R from ligature' },
{ labelAndValue: 'R from stylization' },
{ labelAndValue: 'R from alternative transliteration' },
{ labelAndValue: 'R from Wade–Giles romanization' },
{
labelAndValue: 'From alternative names, general',
disabled: true
},
{ labelAndValue: 'R from alternative language' },
{ labelAndValue: 'R from alternative name' },
{ labelAndValue: 'R from former name' },
{ labelAndValue: 'R from historic name' },
{ labelAndValue: 'R from incomplete name' },
{ labelAndValue: 'R from incorrect name' },
{ labelAndValue: 'R from letter–word combination' },
{ labelAndValue: 'R from long name' },
{ labelAndValue: 'R from portmanteau' },
{ labelAndValue: 'R from predecessor company name' },
{ labelAndValue: 'R from short name' },
{ labelAndValue: 'R from sort name' },
{ labelAndValue: 'R from less specific name' },
{ labelAndValue: 'R from more specific name' },
{ labelAndValue: 'R from antonym' },
{ labelAndValue: 'R from eponym' },
{ labelAndValue: 'R from synonym' },
{ labelAndValue: 'R from Roman numerals' },
{
labelAndValue: 'From alternative names, geography',
disabled: true
},
{ labelAndValue: 'R from Canadian settlement name' },
{ labelAndValue: 'R from name and country' },
{ labelAndValue: 'R from city and state' },
{ labelAndValue: 'R from city and province' },
{ labelAndValue: 'R from more specific geographic name' },
{ labelAndValue: 'R from postal abbreviation' },
{ labelAndValue: 'R from postal code' },
{ labelAndValue: 'R from US postal abbreviation' },
{
labelAndValue: 'From alternative names, organisms',
disabled: true
},
{ labelAndValue: 'R from scientific abbreviation' },
{ labelAndValue: 'R from scientific name' },
{ labelAndValue: 'R from alternative scientific name' },
{ labelAndValue: 'R from monotypic taxon' },
{
labelAndValue: 'From alternative names, people',
disabled: true
},
{ labelAndValue: 'R from birth name' },
{ labelAndValue: 'R from given name' },
{ labelAndValue: 'R from married name' },
{ labelAndValue: 'R from name with title' },
{ labelAndValue: 'R from non-neutral name' },
{ labelAndValue: 'R from personal name' },
{ labelAndValue: 'R from pseudonym' },
{ labelAndValue: 'R from relative' },
{ labelAndValue: 'R from spouse' },
{ labelAndValue: 'R from surname' },
{
labelAndValue: 'From alternative names, technical',
disabled: true
},
{ labelAndValue: 'R from Bluebook abbreviation' },
{ labelAndValue: 'R from brand name' },
{ labelAndValue: 'R from drug trade name' },
{ labelAndValue: 'R from file name' },
{ labelAndValue: 'R from Java package name' },
{ labelAndValue: 'R from MathSciNet abbreviation' },
{ labelAndValue: 'R from molecular formula' },
{ labelAndValue: 'R from NLM abbreviation' },
{ labelAndValue: 'R from product name' },
{ labelAndValue: 'R from slogan' },
{ labelAndValue: 'R from symbol' },
{ labelAndValue: 'R from systematic abbreviations' },
{ labelAndValue: 'R from technical name' },
{ labelAndValue: 'R from trademark' },
{
labelAndValue: 'From – navigation',
disabled: true
},
{ labelAndValue: 'R from file metadata link' },
{ labelAndValue: 'R mentioned in hatnote' },
{ labelAndValue: 'R from shortcut' },
{ labelAndValue: 'R from template shortcut' },
{
labelAndValue: 'From disambiguations',
disabled: true
},
{ labelAndValue: 'R from ambiguous term' },
{ labelAndValue: 'R from incomplete disambiguation' },
{ labelAndValue: 'R from incorrect disambiguation' },
{ labelAndValue: 'R from other disambiguation' },
{ labelAndValue: 'R from predictable disambiguation' },
{ labelAndValue: 'R from unnecessary disambiguation' },
{
labelAndValue: 'From mergers, duplicates, and moves',
disabled: true
},
{ labelAndValue: 'R from duplicated article' },
{ labelAndValue: 'R with history' },
{ labelAndValue: 'R from merge' },
{ labelAndValue: 'R from move' },
{ labelAndValue: 'R with old history' },
{
labelAndValue: 'From fiction',
disabled: true
},
{ labelAndValue: 'R from fictional character' },
{ labelAndValue: 'R from fictional element' },
{ labelAndValue: 'R from fictional location' },
{
labelAndValue: 'From related info',
disabled: true
},
{ labelAndValue: 'R from album' },
{ labelAndValue: 'R from animal' },
{ labelAndValue: 'R from book' },
{ labelAndValue: 'R from catchphrase' },
{ labelAndValue: 'R from domain name' },
{ labelAndValue: 'R from top-level domain' },
{ labelAndValue: 'R from film' },
{ labelAndValue: 'R from gender' },
{ labelAndValue: 'R from legislation' },
{ labelAndValue: 'R from list topic' },
{ labelAndValue: 'R from member' },
{ labelAndValue: 'R from person' },
{ labelAndValue: 'R from phrase' },
{ labelAndValue: 'R from quotation' },
{ labelAndValue: 'R from related word' },
{ labelAndValue: 'R from school' },
{ labelAndValue: 'R from song' },
{ labelAndValue: 'R from subtopic' },
{ labelAndValue: 'R from team' },
{ labelAndValue: 'R from work' },
{ labelAndValue: 'R from writer' },
{ labelAndValue: 'R from Unicode' },
{
labelAndValue: 'To – grammar, punctuation, and spelling',
disabled: true
},
{ labelAndValue: 'R to acronym' },
{ labelAndValue: 'R to initialism' },
{ labelAndValue: 'R to ASCII-only title' },
{ labelAndValue: 'R to diacritic' },
{ labelAndValue: 'R to ligature' },
{ labelAndValue: 'R to plural' },
{
labelAndValue: 'To alternative names',
disabled: true
},
{ labelAndValue: 'R to former name' },
{ labelAndValue: 'R to historic name' },
{ labelAndValue: 'R to joint biography' },
{ labelAndValue: 'R to name with title' },
{ labelAndValue: 'R to monotypic taxon' },
{ labelAndValue: 'R to scientific name' },
{ labelAndValue: 'R to systematic name' },
{ labelAndValue: 'R to technical name' },
{
labelAndValue: 'To – navigation and disambiguation',
disabled: true
},
{ labelAndValue: 'R to anchor' },
{ labelAndValue: 'R to anthroponymy page' },
{ labelAndValue: 'R to disambiguation page' },
{ labelAndValue: 'R to list entry' },
{ labelAndValue: 'R to section' },
{
labelAndValue: 'To miscellaneous',
disabled: true
},
{ labelAndValue: 'R to decade' },
{ labelAndValue: 'R to related topic' },
{ labelAndValue: 'R to subpage' },
{ labelAndValue: 'R to subtopic' },
{ labelAndValue: 'R to TV episode list entry' },
{
label: 'Custom - prompt me',
value: 'custom'
}
]));
} else {
// now Categories
$extra.html('<label for="afcHelper_redirect_name_' + id + '">Category name: </label><input type="text" size="100" ' + 'name="afcHelper_redirect_name_' + id + '" id="afcHelper_redirect_name_' + id + '" value="' + afcHelper_escapeHtmlChars(afcHelper_Submissions[id].title) + '" />');
$extra.html($extra.html() + '<br /><label for="afcHelper_redirect_parents_' + id + '">Parent categories (comma-separated):</label>' + '<input type="text" size="100" id="afcHelper_redirect_parents_' + id + '" name="afcHelper_redirect_parents_' + id + '" value="' + afcHelper_escapeHtmlChars(afcHelper_Submissions[id].parents) + '" />');
$extra.append('<br />');
$extra.append($('<input>', {type: 'checkbox', name: 'afcHelper_redirect_container_' + id, id: 'afcHelper_redirect_container_' + id}));
$extra.append('<label for="afcHelper_redirect_container_' + id + '">This is a <a href="/wiki/Wikipedia:Container_category" title="Wikipedia:Container category">container category</a></label>');
$extra.html($extra.html() + '<br /><input type="checkbox" name="afcHelper_redirect_container_' + id + '"');
}
$extra.html($extra.html() + '<br /><label for="afcHelper_redirect_comment_' + id + '">Comment:</label>' + '<input type="text" size="100" id="afcHelper_redirect_comment_' + id + '" name="afcHelper_redirect_comment_' + id + '"/>');
} else if (selectValue === 'decline') {
if (afcHelper_Submissions[id].type === 'redirect') {
$extra.html('<label for="afcHelper_redirect_decline_' + id + '">Reason for decline: </label>' + afcHelper_generateSelect('afcHelper_redirect_decline_' + id, [
{
label: 'Already exists',
value: 'exists'
}, {
label: 'Blank request',
value: 'blank'
}, {
label: 'No valid target specified',
value: 'notarget'
}, {
label: 'Unlikely search term',
value: 'unlikely'
}, {
label: 'Not a redirect request',
value: 'notredirect'
}, {
label: 'Custom - reason below',
selected: true,
value: 'custom'
}]));
} else {
// now Categories
$extra.html('<label for="afcHelper_redirect_decline_' + id + '">Reason for decline: </label>' + afcHelper_generateSelect('afcHelper_redirect_decline_' + id, [{
label: 'Already exists',
value: 'exists'
}, {
label: 'Blank request',
value: 'blank'
}, {
label: 'Unlikely category',
value: 'unlikely'
}, {
label: 'Not a category request',
value: 'notcategory'
}, {
label: 'Custom - reason below',
selected: true,
value: 'custom'
}]));
}
$extra.html($extra.html() + '<br/><label for="afcHelper_redirect_comment_' + id + '">Comment: </label>' + '<input type="text" size="100" id="afcHelper_redirect_comment_' + id + '" name="afcHelper_redirect_comment_' + id + '"/>');
} else if (selectValue === 'none'){
// for categories and redirects!
$extra.html('');
} else {
$extra.html($extra.html() + '<label for="afcHelper_redirect_comment_' + id + '">Comment: </label>' + '<input type="text" size="100" id="afcHelper_redirect_comment_' + id + '" name="afcHelper_redirect_comment_' + id + '"/>');
}
}
function afcHelper_redirect_performActions() {
// Load all of the data.
for (var i = 0; i < afcHelper_Submissions.length; i++) {
var action = $("#afcHelper_redirect_action_" + i).val();
afcHelper_Submissions[i].action = action;
if (action === 'none') continue;
if (action === 'accept') {
if (afcHelper_Submissions[i].type === 'redirect') {
afcHelper_Submissions[i].title = $("#afcHelper_redirect_from_" + i).val();
afcHelper_Submissions[i].to = $("#afcHelper_redirect_to_" + i).val();
afcHelper_Submissions[i].append = $("#afcHelper_redirect_append_" + i).val();
if (afcHelper_Submissions[i].append === 'custom') {
afcHelper_Submissions[i].append = prompt("Please enter the template to append to " + afcHelper_Submissions[i].title + ". Do not include the curly brackets.");
}
if (afcHelper_Submissions[i].append === 'none' || afcHelper_Submissions[i].append === null) afcHelper_Submissions[i].append = '';
else afcHelper_Submissions[i].append = '\{\{' + afcHelper_Submissions[i].append + '\}\}';
} else {
afcHelper_Submissions[i].title = $("#afcHelper_redirect_name_" + i).val();
afcHelper_Submissions[i].parents = $("#afcHelper_redirect_parents_" + i).val();
afcHelper_Submissions[i].container = $("#afcHelper_redirect_container_" + i).is(':checked');
}
} else if (action === 'decline') {
afcHelper_Submissions[i].reason = $('#afcHelper_redirect_decline_' + i).val();
}
afcHelper_Submissions[i].comment = $("#afcHelper_redirect_comment_" + i).val();
}
// Data loaded. Show progress screen and get WP:AFC/R page text.
displayMessage('<ul id="afcHelper_status"></ul><ul id="afcHelper_finish"></ul>');
var addStatus = function ( status ) { $('#afcHelper_status').append( status ); };
$('#afcHelper_finish').html($('#afcHelper_finish').html() + '<span id="afcHelper_finished_wrapper"><span id="afcHelper_finished_main" style="display:none"><li id="afcHelper_done"><b>Done (<a href="' + mw.config.get("wgArticlePath").replace("$1", encodeURI(afcHelper_RedirectPageName)) + '?action=purge" title="' + afcHelper_RedirectPageName + '">Reload page</a>)</b></li></span></span>');
pagetext = afcHelper_getPageText(afcHelper_RedirectPageName, addStatus);
var totalaccept = 0;
var totaldecline = 0;
var totalcomment = 0;
// traverse the submissions and locate the relevant sections.
addStatus('<li>Processing ' + afcHelper_RedirectSubmissions.length + ' submission' +
(afcHelper_RedirectSubmissions.length === 1 ? '' : 's') + '...</li>');
for (var i = 0; i < afcHelper_RedirectSubmissions.length; i++) {
var sub = afcHelper_RedirectSubmissions[i];
if (pagetext.indexOf(afcHelper_RedirectSections[sub.section]) === -1) {
// Someone has modified the section in the mean time. Skip.
addStatus('<li>Skipping ' + sub.title + ': Cannot find section. Perhaps it was modified in the mean time?</li>'); 
continue;
}
var text = afcHelper_RedirectSections[sub.section];
var startindex = pagetext.indexOf(afcHelper_RedirectSections[sub.section]);
var endindex = startindex + text.length;
// First deal with cats. These are easy.
if (sub.type === 'category') {
if (sub.action === 'accept') {
var cattext = '<!--Created by WP:AFC -->';
if (sub.container) {
cattext += '\n{{Container category}}';
}
if (sub.parents !== '') {
cattext = sub.parents
.split(',')
.map(function(cat){ return '\[\[' + cat + '\]\]'; })
.join('\n');
}
afcHelper_editPage(sub.title, cattext, 'Created via \[\[WP:AFC|Articles for Creation\]\] (\[\[WP:WPAFC|you can help!\]\])', true);
var talktext = '\{\{subst:WPAFC/article|class=Cat\}\}';
var talktitle = sub.title.replace(/Category:/gi, 'Category talk:');
afcHelper_editPage(talktitle, talktext, 'Placing WPAFC project banner', true);
var header = text.match(/==[^=]*==/)[0];
text = header + "\n\{\{AfC-c|a\}\}\n" + text.substring(header.length);
if (sub.comment !== '') text += '\n*\{\{subst:afc category|accept|2=' + sub.comment + '\}\} \~\~\~\~\n';
else text += '\n*\{\{subst:afc category\}\} \~\~\~\~\n';
text += '\{\{AfC-c|b\}\}\n';
totalaccept++;
} else if (sub.action === 'decline') {
var header = text.match(/==[^=]*==/)[0];
var reason = afcHelper_categoryDecline_reasonhash[sub.reason];
if (reason === '') reason = sub.comment;
else if (sub.comment !== '') reason = reason + ': ' + sub.comment;
if (reason === '') {
$('afcHelper_status').html($('#afcHelper_status').html() + '<li>Skipping ' + sub.title + ': No decline reason specified.</li>');   
continue;
}
text = header + "\n\{\{AfC-c|d\}\}\n" + text.substring(header.length);
if (sub.comment === '') text += '\n*\{\{subst:afc category|' + sub.reason + '\}\} \~\~\~\~\n';
else text += '\n*\{\{subst:afc category|decline|2=' + reason + '\}\} \~\~\~\~\n';
text += '\{\{AfC-c|b\}\}\n';
totaldecline++;
} else if (sub.action === 'comment') {
if (sub.comment !== '') text += '\n\n\{\{afc comment|1=' + sub.comment + ' \~\~\~\~\}\}\n';
totalcomment++;
}
} else {
// redirects......
var acceptcomment = '';
var declinecomment = '';
var othercomment = '';
var acceptcount = 0,
declinecount = 0,
commentcount = 0,
hascomment = false;
for (var j = 0; j < sub.from.length; j++) {
var redirect = sub.from[j];
if (redirect.action === 'accept') {
var redirecttext = '#REDIRECT \[\[' + redirect.to + '\]\]\n' + redirect.append;
afcHelper_editPage(redirect.title, redirecttext, 'Redirected page to \[\[' + redirect.to + '\]\] via \[\[WP:AFC|Articles for Creation\]\] (\[\[WP:WPAFC|you can help!\]\])', true);
if ((redirect.title.toLowerCase().indexOf('talk:') < 0) && (redirect.title.indexOf('WT:') < 0)) {
var talktext = '\{\{subst:WPAFC/redirect\}\}';
var talktitle;
if (redirect.title.indexOf(':') >= 0) {
if (redirect.title.indexOf('WP:') == 0) {
talktitle = redirect.title.replace('WP:', 'WT:');
} else {
talktitle = redirect.title.replace(':', ' talk:');
}
} else {
talktitle = 'Talk:' + redirect.title;
}
afcHelper_editPage(talktitle, talktext, 'Placing WPAFC project banner', true);
}
acceptcomment += redirect.title + " &rarr; " + redirect.to;
if (redirect.comment !== '') {
acceptcomment += ': ' + redirect.comment;
hascomment = true;
} else {
acceptcomment += '. ';
}
acceptcount++;
} else if (redirect.action === 'decline') {
var reason = afcHelper_redirectDecline_reasonhash[redirect.reason];
if (reason === '') reason = redirect.comment;
else if (redirect.comment !== '') reason = reason + ': ' + redirect.comment;
if (reason === '') {
$('#afcHelper_status').html($('#afcHelper_status').html() + '<li>Skipping ' + redirect.title + ': No decline reason specified.</li>');  
continue;
}
declinecomment += ((redirect.reason === 'blank' || redirect.reason === 'notredirect') ? reason + ". " : redirect.title + " &rarr; " + redirect.to + ": " + reason + ". ");
declinecount++;
} else if (redirect.action === 'comment') {
othercomment += redirect.title + ": " + redirect.comment + ". ";
commentcount++;
}
}
var reason = '';
if (acceptcount > 0) reason += '\n*\{\{subst:afc redirect|accept|2=' + acceptcomment + ' Thank you for your contributions to Wikipedia!\}\} \~\~\~\~';
if (declinecount > 0) reason += '\n*\{\{subst:afc redirect|decline|2=' + declinecomment + '\}\} \~\~\~\~';
if (commentcount > 0) reason += '\n*\{\{afc comment|1=' + othercomment + '\~\~\~\~\}\}';
reason += '\n';
if (!hascomment && acceptcount === sub.from.length) {
if (acceptcount > 1) reason = '\n*\{\{subst:afc redirect|all\}\} \~\~\~\~\n';
else reason = '\n*\{\{subst:afc redirect\}\} \~\~\~\~\n';
}
if (acceptcount + declinecount + commentcount > 0) {
if (acceptcount + declinecount === sub.from.length) {
// Every request disposed of. Close.
var header = text.match(/==[^=]*==/)[0];
if (acceptcount > declinecount) text = header + "\n\{\{AfC-c|a\}\}\n" + text.substring(header.length);
else text = header + "\n\{\{AfC-c|d\}\}\n" + text.substring(header.length);
text += reason;
text += '\{\{AfC-c|b\}\}\n';
} else text += reason + '\n';
}
totalaccept += acceptcount;
totaldecline += declinecount;
totalcomment += commentcount;
}
pagetext = pagetext.substring(0, startindex) + text + pagetext.substring(endindex);
}
var summary = "Updating submission status:";
if (totalaccept > 0) summary += " accepting " + totalaccept + " request" + (totalaccept > 1 ? 's' : '');
if (totaldecline > 0) {
if (totalaccept > 0) summary += ',';
summary += " declining " + totaldecline + " request" + (totaldecline > 1 ? 's' : '');
}
if (totalcomment > 0) {
if (totalaccept > 0 || totaldecline > 0) summary += ',';
summary += " commenting on " + totalcomment + " request" + (totalcomment > 1 ? 's' : '');
}
afcHelper_editPage(afcHelper_RedirectPageName, pagetext, summary, false);
// Display the "Done" text only after all ajax requests are completed
$(document).ajaxStop(function () {
$("#afcHelper_finished_main").css("display", "");
});
}
/**
 * Gets the text of a page.
 * @param {string} The title of the page to get.
 * @param {function} A function that takes a HTML string to report status.
 */
function afcHelper_getPageText(title, addStatus) {
var addStatus = typeof addStatus !== 'undefined' ? addStatus : function ( status ) {};
addStatus('<li id="afcHelper_get' + jqEsc(title) + '">Getting <a href="' +
mw.config.get("wgArticlePath").replace("$1", encodeURI(title)) + '" title="' + title + '">' +
title + '</a></li>');
var request = {
'action': 'query',
'prop': 'revisions',
'rvprop': 'content',
'format': 'json',
'indexpageids': true,
'titles' : title
};
var response = JSON.parse(
$.ajax({
url: mw.util.wikiScript('api'),
data: request,
async: false
})
.responseText
);
pageid = response['query']['pageids'][0];
if (pageid === "-1") {
addStatus('The page <a class="new" href="' + mw.config.get("wgArticlePath").replace("$1", encodeURI(title)) +
'" title="' + title + '">' + title + '</a> does not exist');
return '';
}
var newtext = response['query']['pages'][pageid]['revisions'][0]['*'];   
addStatus('<li id="afcHelper_get' + jqEsc(title) + '">Got <a href="' +
mw.config.get("wgArticlePath").replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></li>');
return newtext;
}
function afcHelper_cleanuplinks(text) {
// Convert external links to Wikipedia articles to proper wikilinks
var wikilink_re = /(\[){1,2}(?:https?:)?\/\/(en.wikipedia.org\/wiki|enwp.org)\/([^\s\|\]\[]+)(\s|\|)?((?:\[\[[^\[\]]*\]\]|[^\]\[])*)(\]){1,2}/gi;
var temptext = text;
var match;
while (match = wikilink_re.exec(temptext)) {
var pagename = decodeURI(match[3].replace(/_/g,' '));
var displayname = decodeURI(match[5].replace(/_/g,' '));
if (pagename === displayname) displayname = '';
var replacetext = '[[' + pagename + ((displayname) ? '|' + displayname : '') + ']]';
pagetext = pagetext.replace(match[0],replacetext);
}
return text;
}
function afcHelper_generateSelect(title, options) {
return afcHelper_generateSelectObject(title, options).prop('outerHTML');
}
function afcHelper_generateSelectObject(title, options, onchange) {
var $select = $('<select>')
.attr('name', title)
.attr('id', title);
if ( onchange !== null ) {
$select.change(onchange);
}
options.forEach( function ( option ) {
if ( option.labelAndValue ) {
option.value = option.labelAndValue;
option.label = option.labelAndValue;
}
var $option = $( '<option>' )
.appendTo( $select )
.val( afcHelper_escapeHtmlChars( option.value) )
.text( option.label );
if ( option.selected ) $option.attr( 'selected', 'selected' );
if ( option.disabled ) $option.attr( 'disabled', 'disabled' );
} );
return $select;
}
function afcHelper_escapeHtmlChars(original) {
return original.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
/**
 * The old mw.util.jsMessage function before https://gerrit.wikimedia.org/r/#/c/17605/, which
 * introduced the silly auto-hide function. Also with the original styles.
 * Add a little box at the top of the screen to inform the user of
 * something, replacing any previous message.
 * Calling with no arguments, with an empty string or null will hide the message
 * Taken from [[User:Timotheus Canens/displaymessage.js]]
 *
 * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box.
 * @param className {String} Used in adding a class; should be different for each call
 * to allow CSS/JS to hide different boxes. null = no class used.
 * @return {Boolean} True on success, false on failure.
 */
function displayMessage( message, className ){
if ( !arguments.length || message === '' || message === null ) {
$( '#display-message' ).empty().hide();
return true; // Emptying and hiding message is intended behaviour, return true
} else {
// We special-case skin structures provided by the software. Skins that
// choose to abandon or significantly modify our formatting can just define
// an mw-js-message div to start with.
var $messageDiv = $( '#display-message' );
if ( !$messageDiv.length ) {
$messageDiv = $( '<div id="display-message" style="margin:1em;padding:0.5em 2.5%;border:solid 1px #ddd;background-color:#fcfcfc;font-size: 0.8em"></div>' );
if ( mw.util.$content.length ) {
mw.util.$content.prepend( $messageDiv );
} else {
return false;
}
}
if ( className ) {
$messageDiv.prop( 'class', 'display-message-' + className );
}
if ( typeof message === 'object' ) {
$messageDiv.empty();
$messageDiv.append( message );
} else {
$messageDiv.html( message );
}
$messageDiv.slideDown();
return true;
}
}
function jqEsc(expression) {
return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~ ]/g, ''); 
}
function afcHelper_editPage(title, newtext, summary, createonly, nopatrol) {
var wgArticlePath = mw.config.get('wgArticlePath');
summary += afcHelper_advert;
$("#afcHelper_finished_wrapper").html('<span id="afcHelper_AJAX_finished_' + afcHelper_AJAXnumber + '" style="display:none">' + $("#afcHelper_finished_wrapper").html() + '</span>');
var func_id = afcHelper_AJAXnumber;
afcHelper_AJAXnumber++;
$('#afcHelper_status').html($('#afcHelper_status').html() + '<li id="afcHelper_edit' + jqEsc(title) + '">Editing <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></li>');
var request = {
'action': 'edit',
'title': title,
'text': newtext,
'summary': summary,
};
if (createonly) request.createonly = true;
var api = new mw.Api();
api.postWithEditToken(request)
.done(function ( data ) {
if ( data && data.edit && data.edit.result && data.edit.result == 'Success' ) {
$('#afcHelper_edit' + jqEsc(title)).html('Saved <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a>');
} else {
$('#afcHelper_edit' + jqEsc(title)).html('<span class="afcHelper_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + JSON.stringify(data));
window.console && console.error('Edit failed on %s (%s). Error info: %s', wgArticlePath.replace("$1", encodeURI(title)), title, JSON.stringify(data));
}
} )
.fail( function ( error ) {
if (createonly && error == "articleexists")
$('#afcHelper_edit' + jqEsc(title)).html('<span class="afcHelper_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: The article already exists!');
else
$('#afcHelper_edit' + jqEsc(title)).html('<span class="afcHelper_notice"><b>Edit failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + error); 
})
.always( function () {
$("#afcHelper_AJAX_finished_" + func_id).css("display", '');
});
if (!nopatrol) {
/* We patrol by default */
if ($('.patrollink').length) {
// Extract the rcid token from the "Mark page as patrolled" link on page
var patrolhref = $('.patrollink a').attr('href');
var rcid = mw.util.getParamValue('rcid', patrolhref);
if (rcid) {
$('#afcHelper_status').html($('#afcHelper_status').html() + '<li id="afcHelper_patrol' + jqEsc(title) + '">Marking <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + ' as patrolled</a></li>');
var patrolrequest = {
'action': 'patrol',
'format': 'json',
'rcid': rcid
};
api.postWithToken('patrol', patrolrequest)
.done(function ( data ) {
if ( data ) {
$('#afcHelper_patrol' + jqEsc(title)).html('Marked <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a> as patrolled');
} else {
$('#afcHelper_patrol' + jqEsc(title)).html('<span class="afcHelper_notice"><b>Patrolling failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span> with an unknown error');
window.console && console.error('Patrolling failed on %s (%s) with an unknown error.', wgArticlePath.replace("$1", encodeURI(title)), title);
}
} )
.fail( function ( error ) {
$('#afcHelper_patrol' + jqEsc(title)).html('<span class="afcHelper_notice"><b>Patrolling failed on <a href="' + wgArticlePath.replace("$1", encodeURI(title)) + '" title="' + title + '">' + title + '</a></b></span>. Error info: ' + error); 
});
}   
}
}
}
// From http://stackoverflow.com/a/6323598/1757964
function allMatches ( regex, string ) {
var matches = [],
match;
do {
match = regex.exec( string );
if ( match ) {
matches.push( match );
}
} while ( match );
return matches;
}
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
// Create portlet link
var redirectportletLink = mw.util.addPortletLink('p-cactions', '#', 'Review AFC/R', 'ca-afcrhs', 'Review', 'a');
// Bind click handler
$(redirectportletLink).click(function(e) {
e.preventDefault();
// clear variables for the case somebody is clicking on "review" multiple times
afcHelper_RedirectSubmissions.length = 0;
afcHelper_RedirectSections.length = 0;
afcHelper_numTotal = 0;
afcHelper_Submissions.length = 0;
needsupdate.length = 0;
afcHelper_redirect_init();
});
});
})();
//</nowiki>

/* Ahecht/Scripts/pageswap.js */
// <syntaxhighlight lang="javascript">
// [[WP:PMRC#4]] round-robin history swap
// Based on [[:en:User:Andy M. Wang/pageswap.js]] by [[User:Andy M. Wang]] 1.6.1.2018.0920
// Modified by [[User:Ahecht]] -- v1.5.1
/**
 * Initialize variables
 */
if (typeof pagemoveDoPostMoveCleanup === 'undefined') { pagemoveDoPostMoveCleanup = true; }
var pagemoveLink = "[[:en:User:Ahecht/Scripts/pageswap|pageswap]]";
$(document).ready(function() {
mw.loader.using( [
'mediawiki.api',
'mediawiki.util',
] ).then( function() {
"use strict";
/**
 * If user is able to perform swaps
 */
function checkUserPermissions() {
var ret = {};
ret.canSwap = true;
var reslt = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { mw.notify("Swapping pages unavailable.", { title: 'Page Swap Error', type: 'error' }); return ret; },
data: { action:'query', format:'json', meta:'userinfo', uiprop:'rights' }
}).responseText).query.userinfo;
// check userrights for suppressredirect and move-subpages
var rightslist = reslt.rights;
ret.canSwap =
$.inArray('suppressredirect', rightslist) > -1
 && $.inArray('move-subpages', rightslist) > -1;
ret.allowSwapTemplates =
$.inArray('templateeditor', rightslist) > -1;
return ret;
}
/**
 * Given namespace data, title, title namespace, returns expected title of page
 * Along with title without prefix
 * Precondition, title, titleNs is a subject page!
 */
function getTalkPageName(nsData, title, titleNs) {
var ret = {};
var prefixLength = nsData['' + titleNs]['*'].length === 0
? 0 : nsData['' + titleNs]['*'].length + 1;
ret.titleWithoutPrefix = title.substring(prefixLength, title.length);
ret.talkTitle = nsData['' + (titleNs + 1)]['*'] + ':'
+ ret.titleWithoutPrefix;
return ret;
}
/**
 * Given two (normalized) titles, find their namespaces, if they are redirects,
 * if have a talk page, whether the current user can move the pages, suggests
 * whether movesubpages should be allowed, whether talk pages need to be checked
 */
function swapValidate(titleOne, titleTwo, pagesData, nsData, uPerms) {
var ret = {};
ret.valid = true;
if (titleOne === null || titleTwo === null || pagesData === null) {
ret.valid = false;
ret.invalidReason = "Unable to validate swap.";
return ret;
}
ret.allowMoveSubpages = true;
ret.checkTalk = true;
var count = 0;
for (var k in pagesData) {
++count;
if (k == "-1" || pagesData[k].ns < 0) {
ret.valid = false;
ret.invalidReason = ("Page " + pagesData[k].title + " does not exist.");
return ret;
}
// enable only in ns 0..5,12,13,118,119 (Main,Talk,U,UT,WP,WT,H,HT,D,DT)
if ((pagesData[k].ns >= 6 && pagesData[k].ns <= 9)
 || (pagesData[k].ns >= 10 && pagesData[k].ns <= 11 && !uPerms.allowSwapTemplates)
 || (pagesData[k].ns >= 14 && pagesData[k].ns <= 117)
 || (pagesData[k].ns >= 120)) {
ret.valid = false;
ret.invalidReason = ("Namespace of " + pagesData[k].title + " ("
+ pagesData[k].ns + ") not supported.\n\nLikely reasons:\n"
+ "- Names of pages in this namespace relies on other pages\n"
+ "- Namespace features heavily-transcluded pages\n"
+ "- Namespace involves subpages: swaps produce many redlinks\n"
+ "\n\nIf the move is legitimate, consider a careful manual swap.");
return ret;
}
if (titleOne == pagesData[k].title) {
ret.currTitle   = pagesData[k].title;
ret.currNs  = pagesData[k].ns;
ret.currTalkId  = pagesData[k].talkid; // could be undefined
ret.currCanMove = pagesData[k].actions.move === '';
ret.currIsRedir = pagesData[k].redirect === '';
}
if (titleTwo == pagesData[k].title) {
ret.destTitle   = pagesData[k].title;
ret.destNs  = pagesData[k].ns;
ret.destTalkId  = pagesData[k].talkid; // could be undefined
ret.destCanMove = pagesData[k].actions.move === '';
ret.destIsRedir = pagesData[k].redirect === '';
}
}
if (!ret.valid) return ret;
if (!ret.currCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.currTitle + " is immovable. Aborting");
return ret;
}
if (!ret.destCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.destTitle + " is immovable. Aborting");
return ret;
}
if (ret.currNs % 2 !== ret.destNs % 2) {
ret.valid = false;
ret.invalidReason = "Namespaces don't match: one is a talk page.";
return ret;
}
if (count !== 2) {
ret.valid = false;
ret.invalidReason = "Pages have the same title. Aborting.";
return ret;
}
ret.currNsAllowSubpages = nsData['' + ret.currNs].subpages !== '';
ret.destNsAllowSubpages = nsData['' + ret.destNs].subpages !== '';
// if same namespace (subpages allowed), if one is subpage of another,
// disallow movesubpages
if (ret.currTitle.startsWith(ret.destTitle + '/')
|| ret.destTitle.startsWith(ret.currTitle + '/')) {
if (ret.currNs !== ret.destNs) {
ret.valid = false;
ret.invalidReason = "Strange.\n" + ret.currTitle + " in ns "
+ ret.currNs + "\n" + ret.destTitle + " in ns " + ret.destNs
+ ". Disallowing.";
return ret;
}
ret.allowMoveSubpages = ret.currNsAllowSubpages;
if (!ret.allowMoveSubpages)
ret.addlInfo = "One page is a subpage. Disallowing move-subpages";
}
if (ret.currNs % 2 === 1) {
ret.checkTalk = false; // no need to check talks, already talk pages
} else { // ret.checkTalk = true;
var currTPData = getTalkPageName(nsData, ret.currTitle, ret.currNs);
ret.currTitleWithoutPrefix = currTPData.titleWithoutPrefix;
ret.currTalkName = currTPData.talkTitle;
var destTPData = getTalkPageName(nsData, ret.destTitle, ret.destNs);
ret.destTitleWithoutPrefix = destTPData.titleWithoutPrefix;
ret.destTalkName = destTPData.talkTitle;
// possible: ret.currTalkId undefined, but subject page has talk subpages
}
return ret;
}
/**
 * Given two talk page titles (may be undefined), retrieves their pages for comparison
 * Assumes that talk pages always have subpages enabled.
 * Assumes that pages are not identical (subject pages were already verified)
 * Assumes namespaces are okay (subject pages already checked)
 * (Currently) assumes that the malicious case of subject pages
 *   not detected as subpages and the talk pages ARE subpages
 *   (i.e. A and A/B vs. Talk:A and Talk:A/B) does not happen / does not handle
 * Returns structure indicating whether move talk should be allowed
 */
function talkValidate(checkTalk, talk1, talk2) {
var ret = {};
ret.allowMoveTalk = true;
if (!checkTalk) { return ret; } // currTitle destTitle already talk pages
if (talk1 === undefined || talk2 === undefined) {
mw.notify("Unable to validate talk. Disallowing movetalk to be safe", { title: 'Page Swap Error', type: 'warn' });
ret.allowMoveTalk = false;
return ret;
}
ret.currTDNE = true;
ret.destTDNE = true;
ret.currTCanCreate = true;
ret.destTCanCreate = true;
var talkTitleArr = [talk1, talk2];
if (talkTitleArr.length !== 0) {
var talkData = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { mw.notify("Unable to get info on talk pages.", { title: 'Page Swap Error', type: 'error' }); return ret; },
data: { action:'query', format:'json', prop:'info',
intestactions:'move|create', titles:talkTitleArr.join('|') }
}).responseText).query.pages;
for (var id in talkData) {
if (talkData[id].title === talk1) {
ret.currTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.currTTitle = talkData[id].title;
ret.currTCanMove = talkData[id].actions.move === '';
ret.currTCanCreate = talkData[id].actions.create === '';
ret.currTalkIsRedir = talkData[id].redirect === '';
} else if (talkData[id].title === talk2) {
ret.destTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.destTTitle = talkData[id].title;
ret.destTCanMove = talkData[id].actions.move === '';
ret.destTCanCreate = talkData[id].actions.create === '';
ret.destTalkIsRedir = talkData[id].redirect === '';
} else {
mw.notify("Found pageid ("+talkData[id].title+") not matching given ids ("+talk1+" and "+talk2+").", { title: 'Page Swap Error', type: 'error' }); return {};
}
}
}
ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove)
&& (ret.destTCanCreate && ret.destTCanMove);
return ret;
}
/**
 * Given existing title (not prefixed with "/"), optionally searching for talk,
 *   finds subpages (incl. those that are redirs) and whether limits are exceeded
 * As of 2016-08, uses 2 api get calls to get needed details:
 *   whether the page can be moved, whether the page is a redirect
 */
function getSubpages(nsData, title, titleNs, isTalk) {
if ((!isTalk) && nsData['' + titleNs].subpages !== '') { return { data:[] }; }
var titlePageData = getTalkPageName(nsData, title, titleNs);
var subpages = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { return { error:"Unable to search for subpages. They may exist" }; },
data: { action:'query', format:'json', list:'allpages',
apnamespace:(isTalk ? (titleNs + 1) : titleNs),
apfrom:(titlePageData.titleWithoutPrefix + '/'),
apto:(titlePageData.titleWithoutPrefix + '0'),
aplimit:101 }
}).responseText).query.allpages;
// put first 50 in first arr (need 2 queries due to api limits)
var subpageids = [[],[]];
for (var idx in subpages) {
subpageids[idx < 50 ? 0 : 1].push( subpages[idx].pageid );
}
if (subpageids[0].length === 0) { return { data:[] }; }
if (subpageids[1].length === 51) { return { error:"100+ subpages. Aborting" }; }
var dataret = [];
var subpageData0 = JSON.parse($.ajax({ 
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[0].join('|') }
}).responseText).query.pages;
for (var k0 in subpageData0) {
dataret.push({
title:subpageData0[k0].title,
isRedir:subpageData0[k0].redirect === '',
canMove:subpageData0[k0].actions.move === ''
});
}
if (subpageids[1].length === 0) { return { data:dataret }; }
var subpageData1 = JSON.parse($.ajax({ 
url: mw.util.wikiScript('api'), async: false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[1].join('|') }
}).responseText).query.pages;
for (var k1 in subpageData1) {
dataret.push({
title:subpageData1[k1].title,
isRedir:subpageData1[k1].redirect === '',
canMove:subpageData1[k1].actions.move === ''
});
}
return { data:dataret };
}
/**
 * Prints subpage data given retrieved subpage information returned by getSubpages
 * Returns a suggestion whether movesubpages should be allowed
 */
function printSubpageInfo(basepage, currSp) {
var ret = {};
var currSpArr = [];
var currSpCannotMove = [];
var redirCount = 0;
for (var kcs in currSp.data) {
if (!currSp.data[kcs].canMove) {
currSpCannotMove.push(currSp.data[kcs].title);
}
currSpArr.push((currSp.data[kcs].isRedir ? "(R) " : "  ")
+ currSp.data[kcs].title);
if (currSp.data[kcs].isRedir)
redirCount++;
}
if (currSpArr.length > 0) {
alert((currSpCannotMove.length > 0
? "Disabling move-subpages.\n"
+ "The following " + currSpCannotMove.length + " (of "
+ currSpArr.length + ") total subpages of "
+ basepage + " CANNOT be moved:\n\n  "
+ currSpCannotMove.join("\n  ") + '\n\n'
: (currSpArr.length + " total subpages of " + basepage + ".\n"
+ (redirCount !== 0 ? ('' + redirCount + " redirects, labeled (R)\n") : '')
+ '\n' + currSpArr.join('\n'))));
}
ret.allowMoveSubpages = currSpCannotMove.length === 0;
ret.noNeed = currSpArr.length === 0;
return ret;
}
function doDoneMsg(doneMsg) {
if (/failed/ig.test(doneMsg)) { 
mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'warn' });
} else { 
mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'success' });
}
setTimeout(() => {
if(confirm(doneMsg
+ "\nPlease create new red-linked talk pages/subpages if there are incoming links"
+ "\n  (check your contribs for \"Talk:\" redlinks),"
+ "\n  correct any moved redirects, and do post-move cleanup if necessary.\n"
+ "\nOpen contribs page?"))
{
window.open(mw.util.getUrl("Special:Contributions")+'/'+mw.util.wikiUrlencode(mw.user.getName()));
}
}, 250);
}
function createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg) {
if (movedTalk) {
var fromTalk, toTalk;
if (vTData.currTDNE && !vTData.destTDNE) {
fromTalk = vData.destTalkName;
toTalk = vData.currTalkName;
} else if (vTData.destTDNE && !vTData.currTDNE) {
fromTalk = vData.currTalkName;
toTalk = vData.destTalkName;
}
if (fromTalk && toTalk) {
mw.notify("Talk page moved...", { tag: 'status', title: 'Page Swap Status' });
setTimeout(() => {
if (confirm(doneMsg + "\nCreate redirect " + fromTalk
+ "\n→ " + toTalk + " if possible?")) {
var talkRedirect = {
action:'edit',
title:fromTalk,
createonly: true,
text: "#REDIRECT [[" + toTalk + "]]\n{{R from move}}",
summary: "Create redirect to [[" + toTalk + "]] using " + pagemoveLink,
watchlist:"unwatch"
};
mw.notify("Creating talk page redirect...", { tag: 'status', title: 'Page Swap Status' });
new mw.Api().postWithToken("csrf", talkRedirect).done(function (resltc) {
doDoneMsg("Redirect " + fromTalk
+ "\n→ " +toTalk + " created.\n");
}).fail(function (resltc) {
doDoneMsg("Failed to create redirect: " + resltc + ".\n");
});
} else { doDoneMsg(""); }
}, 250);
} else { doDoneMsg(doneMsg); }
} else { doDoneMsg(doneMsg); }
}
/**
 * After successful page swap, post-move cleanup:
 * Make talk page redirect
 * TODO more reasonable cleanup/reporting as necessary
 * vData.(curr|dest)IsRedir
 */
/** TO DO:
 *Check if talk is self redirect
 */
function doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, current = "currTitle", destination = "destTitle") {
if (typeof doneMsg === 'undefined') {
doneMsg = "Moves completed successfully.\n";
mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'success' });
}
// Check for self redirect
var rData = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { mw.notify("Unable to get info about " + vData[current] + ".\n", { title: 'Page Swap Error', type: 'error' }); },
data: { action:'query', format:'json', redirects:'true', titles: vData[current] }
}).responseText).query;
if (rData && rData.redirects && rData.redirects[0].from == rData.redirects[0].to){
var parseData = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { mw.notify("Unable to fetch contents of " + vData[current] + ".\n", { title: 'Page Swap Error', type: 'error' });},
data: {action:'parse', format:'json', prop:'wikitext', page: vData[current] }
}).responseText).parse;
if (parseData) {
var newWikitext = parseData.wikitext['*'].replace(/^#REDIRECT +\[\[ *.* *\]\]/i, ('#REDIRECT [[' + vData[destination] + ']]'));
if (newWikitext != parseData.wikitext['*']) {
mw.notify("Retargeting redirect at " + vData[current] + " to "+ vData[destination] + "...", { tag: 'status', title: 'Page Swap Status' });
new mw.Api().postWithToken("csrf", {
action:'edit',
title: vData[current],
text: newWikitext,
summary : "Retarget redirect to [[" 
+ vData[destination] + "]] using "
+ pagemoveLink,
watchlist: "unwatch"
} ).done(function (resltc) {
doneMsg = doneMsg + "Redirect at " 
+ vData[current] + " retargeted to "
+ vData[destination] + ".\n";
if (current == "currTitle") {
doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
} else {
createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
}
} ).fail(function (resltc) {
doneMsg = doneMsg + "Failed to retarget redirect at "
+ vData[current] + " to "
+ vData[destination] + ": " + resltc + ".\n";
if (current == "currTitle") {
doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
} else {
createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
}
} );
return;
} else {
doneMsg = doneMsg + "Failed to retarget redirect at "
+ vData[current] + " to " + vData[destination]
+ ": String not found.\n";
}
} else {
doneMsg = doneMsg + "Failed to check contents of"
+ vData[current] + ": " + err + ".\n";
}
}
if (current == "currTitle") {
doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
} else {
createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
}
}

/**
 * Swaps the two pages (given all prerequisite checks)
 * Optionally moves talk pages and subpages
 */
function swapPages(titleOne, titleTwo, moveReason, intermediateTitlePrefix,
moveTalk, moveSubpages, vData, vTData) {
if (titleOne === null || titleTwo === null
|| moveReason === null || moveReason === '') {
mw.notify("Titles are null, or move reason given was empty. Swap not done", { title: 'Page Swap Error', type: 'error' });
return false;
}
var intermediateTitle = intermediateTitlePrefix + titleOne;
var pOne = { action:'move', from:titleTwo, to:intermediateTitle,
reason:"[[WP:PMRC#4|Round-robin history swap]] step 1 using " + pagemoveLink,
watchlist:"unwatch", noredirect:1 };
var pTwo = { action:'move', from:titleOne, to:titleTwo,
reason:moveReason,
watchlist:"unwatch", noredirect:1 };
var pTre = { action:'move', from:intermediateTitle, to:titleOne,
reason:"[[WP:PMRC#4|Round-robin history swap]] step 3 using " + pagemoveLink,
watchlist:"unwatch", noredirect:1 };
if (moveTalk) {
pOne.movetalk = 1; pTwo.movetalk = 1; pTre.movetalk = 1;
}
if (moveSubpages) {
pOne.movesubpages = 1; pTwo.movesubpages = 1; pTre.movesubpages = 1;
}
mw.notify("Doing round-robin history swap step 1...", { tag: 'status', title: 'Page Swap Status' });
new mw.Api().postWithToken("csrf", pOne).done(function (reslt1) {
mw.notify("Doing round-robin history swap step 2...", { tag: 'status', title: 'Page Swap Status' });
new mw.Api().postWithToken("csrf", pTwo).done(function (reslt2) {
mw.notify("Doing round-robin history swap step 3...", { tag: 'status', title: 'Page Swap Status' });
new mw.Api().postWithToken("csrf", pTre).done(function (reslt3) {
if (pagemoveDoPostMoveCleanup) {
doPostMoveCleanup(moveTalk, moveSubpages, vData, vTData);
} else {
doDoneMsg("Moves completed successfully.\n");
}
}).fail(function (reslt3) {
doDoneMsg("Fail on third move " + intermediateTitle + " → " + titleOne + "\n");
});
}).fail(function (reslt2) {
doDoneMsg("Fail on second move " + titleOne + " → " + titleTwo + "\n");
});
}).fail(function (reslt1) {
doDoneMsg("Fail on first move " + titleTwo + " → " + intermediateTitle + "\n");
});
}
/**
 * Given two titles, normalizes, does prerequisite checks for talk/subpages,
 * prompts user for config before swapping the titles
 */
function roundrobin(uPerms, currNs, currTitle, destTitle, intermediateTitlePrefix) {
// get ns info (nsData.query.namespaces)
var nsData = JSON.parse($.ajax({ 
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) { mw.notify("Unable to get info about namespaces", { title: 'Page Swap Error', type: 'error' }); },
data: { action:'query', format:'json', meta:'siteinfo', siprop:'namespaces' }
}).responseText).query.namespaces;
// get page data, normalize titles
var relevantTitles = currTitle + "|" + destTitle;
var pagesData = JSON.parse($.ajax({ 
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
mw.notify("Unable to get info about " + currTitle + " or " + destTitle, { title: 'Page Swap Error', type: 'error' });
},
data: { action:'query', format:'json', prop:'info', inprop:'talkid',
intestactions:'move|create', titles:relevantTitles }
}).responseText).query;
for (var kp in pagesData.normalized) {
if (currTitle == pagesData.normalized[kp].from) { currTitle = pagesData.normalized[kp].to; }
if (destTitle == pagesData.normalized[kp].from) { destTitle = pagesData.normalized[kp].to; }
}
// validate namespaces, not identical, can move
var vData = swapValidate(currTitle, destTitle, pagesData.pages, nsData, uPerms);
if (!vData.valid) { mw.notify(vData.invalidReason, { title: 'Page Swap Error', type: 'error' }); return; }
if (vData.addlInfo !== undefined) { mw.notify(vData.addlInfo, { title: 'Page Swap Error', type: 'error' }); }
// subj subpages
var currSp = getSubpages(nsData, vData.currTitle, vData.currNs, false);
if (currSp.error !== undefined) { mw.notify(currSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
var currSpFlags = printSubpageInfo(vData.currTitle, currSp);
var destSp = getSubpages(nsData, vData.destTitle, vData.destNs, false);
if (destSp.error !== undefined) { mw.notify(destSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
var destSpFlags = printSubpageInfo(vData.destTitle, destSp);
var vTData = talkValidate(vData.checkTalk, vData.currTalkName, vData.destTalkName);
// future goal: check empty subpage DESTINATIONS on both sides (subj, talk)
//   for create protection. disallow move-subpages if any destination is salted
var currTSp = getSubpages(nsData, vData.currTitle, vData.currNs, true);
if (currTSp.error !== undefined) { mw.notify(currTSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
var currTSpFlags = printSubpageInfo(vData.currTalkName, currTSp);
var destTSp = getSubpages(nsData, vData.destTitle, vData.destNs, true);
if (destTSp.error !== undefined) { mw.notify(destTSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
var destTSpFlags = printSubpageInfo(vData.destTalkName, destTSp);
var noSubpages = currSpFlags.noNeed && destSpFlags.noNeed
&& currTSpFlags.noNeed && destTSpFlags.noNeed;
// If one ns disables subpages, other enables subpages, AND HAS subpages,
//   consider abort. Assume talk pages always safe (TODO fix)
var subpageCollision = (vData.currNsAllowSubpages && !destSpFlags.noNeed)
|| (vData.destNsAllowSubpages && !currSpFlags.noNeed);
var moveSubpages = false;
// TODO future: currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages
// needs to be separate check. If talk subpages immovable, should not affect subjspace
if (!subpageCollision && !noSubpages && vData.allowMoveSubpages
&& (currSpFlags.allowMoveSubpages && destSpFlags.allowMoveSubpages)
&& (currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages)) {
moveSubpages = confirm("Move subpages? (OK for yes, Cancel for no)");
} else if (subpageCollision) {
mw.notify("One namespace does not have subpages enabled. Disallowing move subpages", { title: 'Page Swap Error', type: 'error' });
}
var moveTalk = false;
// TODO: count subpages and make restrictions?
if (vData.checkTalk && (!vTData.currTDNE || !vTData.destTDNE || moveSubpages)) {
if (vTData.allowMoveTalk) {
moveTalk = confirm("Move talk page(s)? (OK for yes, Cancel for no)");
} else {
alert("Disallowing moving talk. "
+ (!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected")
: (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected")
: "Talk page is immovable")));
}
}
var moveReason = '';
var moveReasonPrompt = '';
if (typeof mw.util.getParamValue("wpReason") === 'string') {
moveReasonPrompt = mw.util.getParamValue("wpReason");
} else if (document.getElementsByName("wpReason")[0] && document.getElementsByName("wpReason")[0].value != '') {
moveReasonPrompt = document.getElementsByName("wpReason")[0].value;
} else if (typeof moveReasonDefault === 'string') {
moveReasonPrompt = moveReasonDefault;
}
moveReason = prompt("Move reason:", moveReasonPrompt);
var confirmString = "Round-robin configuration:\n  "
+ currTitle + " → " + destTitle + "\n: " + moveReason
+ "\n  with movetalk:" + moveTalk + ", movesubpages:" + moveSubpages
+ "\n\nProceed? (Cancel to abort)";
if (confirm(confirmString)) {
swapPages(currTitle, destTitle, moveReason, intermediateTitlePrefix,
moveTalk, moveSubpages, vData, vTData);
}
}
var currNs = mw.config.get("wgNamespaceNumber");
var wpOldTitle = mw.util.getParamValue("wpOldTitle");
if (!wpOldTitle && document.getElementsByName("wpOldTitle")[0] && document.getElementsByName("wpOldTitle")[0].value != ''){
wpOldTitle = document.getElementsByName("wpOldTitle")[0].value;
}
var wpNewTitle = mw.util.getParamValue("wpNewTitle");
if (!wpNewTitle && document.getElementsByName("wpNewTitleMain")[0] && document.getElementsByName("wpNewTitleMain")[0].value != '' && document.getElementsByName("wpNewTitleNs")[0]){
wpNewTitle = document.getElementsByName("wpNewTitleMain")[0].value;
var nsid = document.getElementsByName("wpNewTitleNs")[0].value;
if (nsid != 0) {
wpNewTitle = mw.config.get("wgFormattedNamespaces")[nsid] + ":" + wpNewTitle;
}
}
if (currNs < -1 || currNs >= 120
|| (currNs >=  6 && currNs <= 9)
|| (currNs >= 14 && currNs <= 99)
|| (currNs == -1 && mw.config.get("wgCanonicalSpecialPageName") != "Movepage")
|| (mw.config.get("wgCanonicalSpecialPageName") == "Movepage" && !wpOldTitle)
)
return; // special/other page
var portletLink = mw.util.addPortletLink("p-cactions", "#", "Swap",
"ca-swappages", "Perform a revision history swap / round-robin move");
$( portletLink ).click(function(e) {
e.preventDefault();
var userPermissions = checkUserPermissions();
if (!userPermissions.canSwap) {
mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' }); return;
}
var currTitle = wpOldTitle || mw.config.get("wgPageName");
var destTitle = wpNewTitle || prompt("Swap \"" + (currTitle.replace(/_/g, ' ')) + "\" with:", (currTitle.replace(/_/g, ' ')));
return roundrobin(userPermissions, currNs, currTitle, destTitle, "Draft:Move/");
});
if (mw.config.get("wgCanonicalSpecialPageName") == "Movepage" 
&& $( "div.mw-message-box-error" ).find( "p" ).eq(1).is( ":contains('name already exists')" )
&& wpOldTitle)
{
$( "div.mw-message-box-error" ).find( "p" ).eq(2).html( 'Please choose another name, or perform a <a title="Perform a revision history swap / round-robin move" href="#" id="pageswapLink">swap</a>.' );
$( "#pageswapLink" ).click(function(e) {
e.preventDefault();
$( portletLink ).click();
});
}
});
});
// </syntaxhighlight>

/* Enterprisey/delsort.js */
//<nowiki>
( function ( $, mw ) {
mw.loader.load( "jquery.chosen" );
mw.loader.load( "mediawiki.ui.input", "text/css" );
var afdcCategories = { "m": "Media and music", "o": "Organization, corporation, or product", "b": "Biographical", "s": "Society topics", "w": "Web or Internet", "g": "Games or sports", "t": "Science and technology", "f": "Fiction and the arts", "p": "Places and transportation", "i": "Indiscernible or unclassifiable topic", "u": "Not sorted yet" };
var ADVERTISEMENT = " ([[User:Enterprisey/delsort|assisted]])";
var currentAfdcCat = "";
var currentDelsortCategories = [];
if ( mw.config.get( "wgPageName" ).indexOf("Wikipedia:Articles_for_deletion/") != -1 &&
 mw.config.get( "wgPageName" ).indexOf("Wikipedia:Articles_for_deletion/Log/") == -1) {
var portletLink = mw.util.addPortletLink("p-cactions", "#", "Delsort", "pt-delsort", "Perform deletion sorting");
// Load list of delsort categories
var delsortCategoriesPromise = $.ajax( {
url: "https://en.wikipedia.org/w/index.php?action=raw&title=" + encodeURIComponent( "Wikipedia:WikiProject Deletion sorting/Computer-readable.json" ) + "&maxage=86400&smaxage=86400",
dataType: "json"
} )
$( portletLink ).click( function ( e ) {
e.preventDefault();
// Validation for new custom fields
var validateCustomCat = function ( container ) {
var categoryName = container.children( "input" ).first().val();
$.getJSON(
mw.util.wikiScript("api"),
{
format: "json",
action: "query",
prop: "pageprops",
titles: "Wikipedia:WikiProject Deletion sorting/" + categoryName
}
).done( function ( data ) {
var setStatus = function ( status ) {
var text = "Not sure";
var imageSrc = "https://upload.wikimedia.org/wikipedia/commons/a/ad/Question_mark_grey.png";
switch( status ) {
case "d":
text = "Doesn't exist";
imageSrc = "https://upload.wikimedia.org/wikipedia/commons/5/5f/Red_X.svg";
break;
case "e":
text = "Exists";
imageSrc = "https://upload.wikimedia.org/wikipedia/commons/1/16/Allowed.svg";
break;
}
container.children( ".category-status" ).empty()
.append( $( "<img>", { "src": imageSrc,
"style": "padding: 0 5px; width: 20px; height: 20px" } ) )
.append( text );
};
if( data && data.query && data.query.pages ) {
if( data.query.pages.hasOwnProperty( "-1" ) ) {
setStatus( "d" );
} else {
setStatus( "e" );
}
} else {
setStatus( "n" );
}
} );
};
// Define a function to add a new custom field, used below
var addCustomField = function ( e ) {
$( "<div>" )
.appendTo( "#delsort-td" )
.css( "width", "100%" )
.css( "margin", "0.25em auto" )
.append( $( "<input>" )
 .attr( "type", "text" )
 .addClass( "mw-ui-input mw-ui-input-inline custom-delsort-field" )
 .change( function ( e ) {
 validateCustomCat( $( this ).parent() );
 } ) )
.append( $( "<span>" ).addClass( "category-status" ) )
.append( " (" )
.append( $( "<img>", { "src": "https://upload.wikimedia.org/wikipedia/commons/a/a2/Crystal_128_reload.svg",
"style": "width: 15px; height: 15px; cursor: pointer" } )
 .click( function ( e ) {
 validateCustomCat( $( this ).parent() );
 } ) )
.append( ")" )
.append( $( "<button>" )
 .addClass( "mw-ui-button mw-ui-destructive mw-ui-quiet" )
 .text( "Remove" )
 .click( function () {
 $( this ).parent().remove();
 } ) );
};
$( "#mw-content-text" ).prepend(
'<div style="border: thin solid rgb(197, 197, 197); box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.25); border-radius: 3px; padding: 5px; position: relative;" id="delsort">' +
'  <div id="delsort-title" style="font-size: larger; font-weight: bold; text-align: center;">Select a deletion sorting category</div>' +
'  <table style="margin: 2em auto; border-collapse: collapse;" id="delsort-table">' +
'<tr style="font-size: larger"><th>AFDC</th><th>DELSORT</th></tr>' +
'<tr>' +
'  <td style="padding-right: 10px;">' +
'<table id="afdc">' +
'</table>' +
'  </td>' +
'  <td style="border-left: solid black thick; padding-left: 10px; vertical-align: top;" id="delsort-td">' +
'  <select multiple="multiple" data-placeholder="Select a deletion sorting category..."></select>' +
'  <button id="add-custom-button" class="mw-ui-button mw-ui-progressive mw-ui-quiet">Add custom</button>' +
'  </td>' +
'</tr>' +
'  </table>' +
'  <button style="position: absolute; top: 5px; right: 5px;" id="close-button" class="mw-ui-button mw-ui-destructive mw-ui-quiet">Close</button>' +
'</div>' );
$( "#add-custom-button" ).click( addCustomField );
$( "#close-button" ).click( function () { $( "#delsort" ).remove(); } );
var afdcHtml = "";
Object.keys( afdcCategories ).forEach( function ( code, i ) {
if ( i % 2 === 0 ) afdcHtml += "<tr>";
afdcHtml += "<td><input type='radio' name='afdc' value='" + code + "' id='afdc-" + code + "' /><label for='afdc-" + code + "'>" + afdcCategories[ code ] + "</label></td>";
if ( i % 2 !== 0 ) afdcHtml += "</tr>";
} );
// If there are an odd number of AFDC cats, we need to close off the last row
if ( Object.keys( afdcCategories ).length % 2 !== 0 ) afdcHtml += "</tr>";
$( "#afdc" ).html( afdcHtml );
// Build the deletion sorting categories
delsortCategoriesPromise.done( function ( delsortCategories ) {
$.each( delsortCategories, function ( groupName, categories ) {
var group = $( "<optgroup>" )
.appendTo( "#delsort select" )
.attr( "label", groupName );
$.each( categories, function ( index, category ) {
group.append( $( "<option>" )
  .val( category )
  .text( category )
  .addClass( "delsort-category" ) );
} );
} );
getWikitext( mw.config.get( "wgPageName" ) ).then( function ( wikitext ) {
autofillAfdc( wikitext );
// Autofill the delsort box
var DELSORT_RE = /:<small class="delsort-notice">(.+?)<\/small>/g;
var DELSORT_LIST_RE = /\[\[Wikipedia:WikiProject Deletion sorting\/(.+?)\|.+?\]\]/;
var delsortMatch;
var delsortListMatch;
do {
delsortMatch = DELSORT_RE.exec( wikitext );
if( delsortMatch !== null ) {
delsortListMatch = DELSORT_LIST_RE.exec( delsortMatch[1] );
if( delsortListMatch !== null ) {
currentDelsortCategories.push( delsortListMatch[1] );
var delsortOption = document.querySelector( "option.delsort-category[value='" + delsortListMatch[1] + "']" );
if( delsortOption ) {
delsortOption.selected = true;
}
}
}
} while( delsortMatch );
// Now that we've updated the underlying <select>, ask Chosen to
// update the visible search box
$( "#delsort select" ).trigger( "chosen:updated" );
} ); // end getWikitext
} ); // end delsortCategoriesPromise
// Initialize the special chosen.js select box
// (some code stolen from http://stackoverflow.com/a/27445788)
$( "#delsort select" ).chosen();
$( "#delsort .chzn-container" ).css( "text-align", "left" );
// Add the button that triggers sorting
$( "#delsort" ).append( $( "<div>" )
.css( "text-align", "center" )
.append( $( "<button> ")
.addClass( "mw-ui-button" )
.addClass( "mw-ui-progressive" )
.attr( "id", "sort-button" )
.text( "Save changes" )
.click( function () {
// Make a status list
$( "#delsort" ).append( $( "<ul> ")
.attr( "id", "status" ) );
// Build a list of categories
var categories = $( "#delsort select" ).val() || [];
$( ".custom-delsort-field" ).each( function ( index, element ) {
categories.push( $( element ).val() );
} );
categories = categories.filter( Boolean ); // remove empty strings
categories = removeDups( categories );
// Only allow categories that aren't already there
categories = categories.filter( function ( elem ) {
return currentDelsortCategories.indexOf( elem ) < 0;
} );
// Obtain the target AFDC category, brought to you by http://stackoverflow.com/a/24886483/1757964
var afdcTarget = document.querySelector("input[name='afdc']:checked").value;
// Actually do the delsort
saveChanges( categories, afdcTarget );
} ) ) );
} );
} // End if ( mw.config.get( "wgPageName" ).indexOf('Wikipedia:Articles_for_deletion/') ... )
/*
 * Autofills the AFDC radio button group based on the current
 * page's wikitext
 */
function autofillAfdc( wikitext ) {
var regexMatch = /REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD(?:\|(.*))?}}/.exec( wikitext );
if ( regexMatch ) {
var templateParameter = regexMatch[1];
if ( templateParameter ) {
currentAfdcCat = templateParameter;
if ( templateParameter.length === 1 ) {
var currentClass = templateParameter.toLowerCase();
$( "#afdc-" + currentClass ).prop( "checked", true );
}
}
}
}
/*
 * Saves the changes to the current discussion page by adding delsort notices (if applicable) and updating the AFDC cat
 */
function saveChanges( cats, afdcTarget ) {
var changingAfdcCat = currentAfdcCat.toLowerCase() !== afdcTarget;
// Indicate to the user that we're doing some deletion sorting
$( "#delsort-table" ).remove();
$( "#delsort #sort-button" )
.text( "Sorting " + ( changingAfdcCat ? "and categorizing " : "" ) + "discussion..." )
.prop( "disabled", true )
.fadeOut( 400, function () {
$( this ).remove();
} );
var categoryTitleComponent = ( cats.length === 1 ) ? ( "the \"" + cats[0] + "\" category" ) : ( cats.length + " categories" );
var afdcTitleComponent = changingAfdcCat ? " and categorizing it as " + afdcCategories[ afdcTarget ] : "";
$( "#delsort-title" )
.html( "Sorting discussion into " + categoryTitleComponent + afdcTitleComponent + "<span id=\"delsort-dots\"></span>" );
// Start the animation, using super-advanced techniques
var animationInterval = setInterval( function () {
$( "#delsort-dots" ).text( $( "#delsort-dots" ).text() + "." );
if( $( "#delsort-dots" ).text().length > 3 ) {
$( "#delsort-dots" ).text( "" );
}
}, 600 );
// Place (a) notification(s) on the discussion and update its AFDC cat
var editDiscussionDeferred = postDelsortNoticesAndUpdateAfdc( cats, afdcTarget );
// List the discussion at the DELSORT pages
var deferreds = cats.map( listAtDelsort );
// We still have to wait for the discussion to be edited
deferreds.push( editDiscussionDeferred );
// When everything's done, say something
$.when.apply( $, deferreds ).then( function () {
// Call the done hook
if( window.delsortDoneHook ) {
window.delsortDoneHook();
}
// We're done!
$( "#delsort-title" )
.text( "Done " + ( changingAfdcCat ? "updating the discussion's AFDC category and " : "" ) + "sorting discussion into " + categoryTitleComponent + "." );
showStatus( "<b>Done!</b> " + ( changingAfdcCat ? "The discussion's AFDC was updated and it was" : "Discussion was" ) + " sorted into " + categoryTitleComponent + ". (" )
.append( $( "<a>" )
 .text( "reload" )
 .attr( "href", "#" )
 .click( function () { document.location.reload( true ); } ) )
.append( ")" );
clearInterval( animationInterval );
} );
}
/*
 * Adds a new status to the status list, and returns the newly-displayed element.
 */
function showStatus( newStatus ) {
return $( "<li>" )
 .appendTo( "#delsort ul#status" )
 .html( newStatus );
}
/*
 * Adds some notices to the discussion page that this discussion was sorted.
 */
function postDelsortNoticesAndUpdateAfdc( cats, afdcTarget ) {
var changingAfdcCat = currentAfdcCat.toLowerCase() !== afdcTarget,
deferred = $.Deferred(),
statusElement = showStatus( "Updating the discussion page..." );
getWikitext( mw.config.get( "wgPageName" ) ).then( function ( wikitext ) {
try {
statusElement.html( "Processing wikitext..." );
// Process wikitext
// First, add delsort notices
wikitext += createDelsortNotices( cats );
// Then, update the AFDC category
var afdcMatch = wikitext.match( /REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD/ );
if ( afdcMatch && afdcMatch[ 0 ] ) {
var afdcMatchIndex = wikitext.indexOf( afdcMatch[ 0 ] ) + afdcMatch[ 0 ].length,
charAfterTemplateName = wikitext[ afdcMatchIndex ];
if ( charAfterTemplateName === "}" ) {
wikitext = wikitext.slice( 0, afdcMatchIndex ) + "|" + afdcTarget.toUpperCase() + wikitext.slice( afdcMatchIndex );
} else if ( charAfterTemplateName === "|" ) {
wikitext = wikitext.replace( "|" + currentAfdcCat + "}}", "|" + afdcTarget.toUpperCase() + "}}" );
}
}
statusElement.html( "Processed wikitext. Saving..." );
var catPlural = ( cats.length === 1 ) ? "" : "s";
$.ajax( {
url: mw.util.wikiScript( "api" ),
type: "POST",
dataType: "json",
data: {
format: "json",
action: "edit",
title: mw.config.get( "wgPageName" ),
summary: "Updating nomination page with notices" + ( changingAfdcCat ? " and new AFDC cat" : "" ) + ADVERTISEMENT,
token: mw.user.tokens.get( "csrfToken" ),
text: wikitext
}
} ).done ( function ( data ) {
if ( data && data.edit && data.edit.result && data.edit.result == "Success" ) {
statusElement.html( cats.length + " notice" + catPlural + " placed on the discussion!" );
if ( changingAfdcCat ) {
if ( currentAfdcCat ) {
var formattedCurrentAfdcCat = currentAfdcCat.length === 1 ? afdcCategories[ currentAfdcCat.toLowerCase() ] : currentAfdcCat;
showStatus( "Discussion's AFDC category was changed from " + formattedCurrentAfdcCat + " to " + afdcCategories[ afdcTarget ] + "." );
} else {
showStatus( "Discussion categorized under " + afdcCategories[ afdcTarget ] + " with AFDC." );
}
}
deferred.resolve();
} else {
statusElement.html( "While editing the current discussion page, the edit query returned an error. =(" );
deferred.reject();
}
} ).fail ( function() {
statusElement.html( "While editing the current discussion page, the AJAX request failed." );
deferred.reject();
} );
} catch ( e ) {
statusElement.html( "While getting the current page content, there was an error." );
console.log( "Current page content request error: " + e.message );
deferred.reject();
}
} ).fail( function () {
statusElement.html( "While getting the current content, there was an AJAX error." );
deferred.reject();
} );
return deferred;
}
/*
 * Turns a list of delsort categories into a number of delsort template notice substitutions.
 */
function createDelsortNotices( cats ) {
if ( Array.isArray(cats) && ! cats.length ) return '';
var appendText = "\n{{subst:Deletion sorting/multi";
cats.forEach( function ( cat ) {
appendText += "|" + cat;
} );
return appendText + "|sig=~~" + "~~}}"; // string concat to prevent it from being transformed into my signature
}
/*
 * Adds a listing at the DELSORT page for the category.
 */
function listAtDelsort( cat ) {
// Make a status element just for this category
var statusElement = showStatus( "Listing this discussion at DELSORT/" +
cat + "..." );
// Clarify our watchlist behavior for this edit
var allowedWatchlistBehaviors = ["watch", "unwatch", "preferences",
"nochange"];
var watchlistBehavior = "nochange"; // no watchlist change by default
if( window.delsortWatchlist && allowedWatchlistBehaviors.indexOf(
window.delsortWatchlist.toLowerCase() ) >= 0 ) {
watchlistBehavior = window.delsortWatchlist.toLowerCase();
}
var listTitle = "Wikipedia:WikiProject Deletion sorting/" + cat;
// First, get the current wikitext for the DELSORT page
return $.getJSON(
mw.util.wikiScript("api"),
{
format: "json",
action: "query",
prop: "revisions",
rvprop: "content",
rvslots: "main",
rvlimit: 1,
titles: listTitle,
redirects: "true",
formatversion: 2,
}
).then( function ( data ) {
var wikitext = data.query.pages[0].revisions[0].slots.main.content;
var properTitle = data.query.pages[0].title;
try {
statusElement.html( "Got the DELSORT/" + cat + " listing wikitext, processing..." );
// Actually edit the content to include the new listing
var newDelsortContent = wikitext.replace("directly below this line -->", "directly below this line -->\n\{\{" + mw.config.get("wgPageName") + "\}\}");
// Then, replace the DELSORT listing with the new content
$.ajax( {
url: mw.util.wikiScript( "api" ),
type: "POST",
dataType: "json",
data: {
format: "json",
action: "edit",
title: properTitle,
summary: "Listing [[" + mw.config.get("wgPageName") + "]]" + ADVERTISEMENT,
token: mw.user.tokens.get( "csrfToken" ),
text: newDelsortContent,
watchlist: watchlistBehavior
}
} ).done ( function ( data ) {
if ( data && data.edit && data.edit.result && data.edit.result == "Success" ) {
statusElement.html( "Listed page at <a href=" + mw.util.getUrl( listTitle ) + ">the " + cat + " deletion sorting list</a>!" );
} else {
statusElement.html( "While listing at DELSORT/" + cat + ", the edit query returned an error. =(" );
}
} ).fail ( function() {
statusElement.html( "While listing at DELSORT/" + cat + ", the ajax request failed." );
} );
} catch ( e ) {
statusElement.html( "While getting the DELSORT/" + cat + " content, there was an error." );
console.log( "DELSORT content request error: " + e.message );
//console.log( "DELSORT content request response: " + JSON.stringify( data ) );
}
} ).fail( function () {
statusElement.html( "While getting the DELSORT/" + cat + " content, there was an AJAX error." );
} );
}
/**
 * Gets the wikitext of a page with the given title (namespace required).
 */
function getWikitext( title ) {
return $.getJSON(
mw.util.wikiScript("api"),
{
format: "json",
action: "query",
prop: "revisions",
rvprop: "content",
rvslots: "main",
rvlimit: 1,
titles: title,
formatversion: 2,
}
).then( function ( data ) {
return data.query.pages[0].revisions[0].slots.main.content;
} );
}
/**
 * Removes duplicates from an array.
 */
function removeDups( arr ) {
var obj = {};
for( var i = 0; i < arr.length; i++ ) {
obj[arr[i]] = 0;
}
return Object.keys( obj );
}
}( jQuery, mediaWiki ) );
//</nowiki>