User:Inductiveload/maintain.js

/** * Simple maintenance tools */

/* eslint-disable camelcase, no-var */

'use strict';

// IIFE used when including as a user script (to allow debug or config) // Default gadget use will get an IIFE wrapper as well ( function ( $, mw, OO ) {

window.inductiveload = window.inductiveload || {}; // use window for ResourceLoader compatibility

if ( inductiveload.maintain ) { return; // already initialised, don't overwrite }

inductiveload.maintain = { transforms: {} };

/* -- */

/*	 * Generic wrapper around MW.APi.get *	 * Returns a deferred. Results are resolve'd after passing through the * given filter functions. */	var mw_get_deferred = function ( params, result_filter, failure ) { var deferred = $.Deferred;

new mw.Api.get( params ) .done( function ( data ) {				deferred.resolve( result_filter( data ) );			} ) .fail( function {				deferred.resolve( failure );			} );

return deferred; };

var emptyArrayFunc = function { return []; };	var emptyStringFunc = function { return ''; };

/*	 * Get pages with prefix */	var get_page_suggestions = function ( input, namespaces ) {

var params = { format: 'json', formatversion: 2, action: 'query', list: 'prefixsearch', pslimit: 15, pssearch: input };		if ( namespaces && namespaces.length ) { params.apnamespace = namespaces.join( '|' ); }

return mw_get_deferred( params,			function ( data ) {				return data.query.prefixsearch;			},			emptyArrayFunc ); };

var get_last_edited = function ( user, namespaces ) {

var params = { format: 'json', formatversion: 2, action: 'query', list: 'usercontribs', uclimit: 15, ucuser: user };		if ( namespaces && namespaces.length ) { params.ucnamespace = namespaces.join( '|' ); }

return mw_get_deferred( params,			function ( data ) {				return data.query.usercontribs;			},			emptyArrayFunc ); };

var get_page_wikitext = function ( title ) { var params = { action: 'query', format: 'json', formatversion: 2, prop: 'revisions', rvslots: 'main', rvprop: 'content', titles: title };

return mw_get_deferred( params,			function ( data ) {				return data.query.pages[ 0 ].revisions[ 0 ].slots.main.content;			},			emptyStringFunc		); };

var get_last_contribs = function ( user, limit ) { var params = { action: 'query', format: 'json', formatversion: 2, list: 'usercontribs', ucuser: user, limit: limit };

return mw_get_deferred( params,			function ( data ) {				return data.query.usercontribs;			},			emptyArrayFunc		); };

const getPageWikitext = function ( pageTitle ) { const slot = 'main'; const params = { action: 'query', format: 'json', formatversion: 2, prop: 'revisions', rvslots: slot, rvprop: 'content', titles: pageTitle, rvlimit: 1 };

return mw_get_deferred( params,			function ( data ) {				return data.query.pages[ 0 ].revisions[ 0 ].slots[ slot ].content;			},			emptyArrayFunc		); };

function unique_pages( a ) { var seen = {}; return a.filter( function ( item ) {			return seen.hasOwnProperty( item.pageid ) ? false : ( seen[ item.pageid ] = true );		} ); }

/* -- */

/*	 * A widget which looks up pages from a certain namespace by prefix * If no input is given, it uses the previous edits of a given user */	var PageLookupTextInputWidget = function PageLookupTextInputWidget( config ) { OO.ui.TextInputWidget.call( this, $.extend( { /* validate: 'integer' */ }, config ) ); OO.ui.mixin.LookupElement.call( this, $.extend( { allowSuggestionsWhenEmpty: true }, config ) );

this.namespaces = config.namespaces || []; this.user = config.user; };	OO.inheritClass( PageLookupTextInputWidget, OO.ui.TextInputWidget ); OO.mixinClass( PageLookupTextInputWidget, OO.ui.mixin.LookupElement );

PageLookupTextInputWidget.prototype.getLookupRequest = function {

var value = this.getValue;

var deferred;

if ( !value && this.user ) { deferred = get_last_edited( this.user, this.namespaces ); } else { deferred = get_page_suggestions( value, this.namespaces ); }

return deferred.promise( {			abort: function {}		} ); };

PageLookupTextInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) { return response || []; };

PageLookupTextInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {

var mow_from_page = function ( page ) { return new OO.ui.MenuOptionWidget( {				data: page.title,				label: page.title			} ); };

data = unique_pages( data ); return data.map( mow_from_page ); };

/* -- */

/*	 * A widget which looks up contributions by a certain user */	var ContribLookupTextInputWidget = function ContribLookupTextInputWidget( config ) { OO.ui.TextInputWidget.call( this, $.extend( { /* validate: 'integer' */ }, config ) ); OO.ui.mixin.LookupElement.call( this, $.extend( { allowSuggestionsWhenEmpty: true }, config ) );

this.namespace = config.namespace; this.user = config.user; };	OO.inheritClass( ContribLookupTextInputWidget, OO.ui.TextInputWidget ); OO.mixinClass( ContribLookupTextInputWidget, OO.ui.mixin.LookupElement );

ContribLookupTextInputWidget.prototype.getLookupRequest = function { var ns = this.namespace ? [ this.namespace ] : undefined;

var deferred = get_last_contribs( this.user, ns );

return deferred.promise( {			abort: function {}		} ); };

ContribLookupTextInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) { return response || []; };

ContribLookupTextInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {

var mow_from_contrib = function ( edit ) { var time = edit.timestamp.replace( 'Z', '' ).replace( 'T', ' ' ); var size = ( ( edit.size >= 0 ) ? '+' : '' ) + String( edit.size );

var content = [ new OO.ui.HtmlSnippet( "" +					edit.revid + ' (' + size + ', ' + time + ') ' ) ];

if ( edit.comment ) { content.push(					new OO.ui.HtmlSnippet( "" + edit.comment + ' ' )				); }

return new OO.ui.MenuOptionWidget( {				data: edit.revid,				text: edit.title,				content: content			} ); };

return data.map( mow_from_contrib ); };

/* -- */

/*	 * A widget which looks up wikitext lines from a page, optionally matching filters */	var WikitextLineLookup = function ( config ) { OO.ui.TextInputWidget.call( this, $.extend( { /* validate: 'integer' */ }, config ) ); OO.ui.mixin.LookupElement.call( this, $.extend( { allowSuggestionsWhenEmpty: true }, config ) );

this.page = config.page;

const filters = config.filters;

this.wikitextPromise = getPageWikitext( this.page ) .then( ( wikitext ) => {				let lines = wikitext.split( '\n' );

if ( filters ) { lines = lines.filter( ( line ) => {						for ( const filter of filters ) {							if ( filter.test( line ) ) {								return true;							}						}						return false;					} ); }

this.wikitextLines = lines; } );	};	OO.inheritClass( WikitextLineLookup, OO.ui.TextInputWidget );	OO.mixinClass( WikitextLineLookup, OO.ui.mixin.LookupElement );

WikitextLineLookup.prototype.getLookupRequest = function { const deferred = $.Deferred;

const value = this.getValue.toLowerCase; const matches = this.wikitextLines.filter( ( l ) => {			return l.toLowerCase.indexOf( value ) !== -1;		} );

// wait for the wikitext to load this.wikitextPromise.then( => {			deferred.resolve( matches );		} );

return deferred.promise( {			abort: function {}		} ); };

WikitextLineLookup.prototype.getLookupCacheDataFromResponse = function ( response ) { return response || []; };

WikitextLineLookup.prototype.getLookupMenuOptionsFromData = function ( data ) {

var mowFromLine = function ( line ) { return new OO.ui.MenuOptionWidget( {				data: line,				text: line			} ); };

return data.map( mowFromLine ); };

/* -- */

function PrimaryActionOnEnterMixin( /* config */ ) { this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this ); }	OO.initClass( PrimaryActionOnEnterMixin );

PrimaryActionOnEnterMixin.prototype.onDialogKeyDown = function ( e ) { var actions; if ( e.which === OO.ui.Keys.ENTER ) { actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } ); if ( actions.length > 0 ) { this.executeAction( actions[ 0 ].getAction ); e.preventDefault; e.stopPropagation; }		} else if ( e.which === OO.ui.Keys.ESCAPE ) { actions = this.actions.get( { flags: 'safe', visible: true, disabled: false } ); this.executeAction( actions[ 0 ].getAction ); e.preventDefault; e.stopPropagation; }	};

/* -- */

var dialogs = {};

dialogs.ActionChooseDialog = function ( config ) { dialogs.ActionChooseDialog.super.call( this, config ); // mixin constructors PrimaryActionOnEnterMixin.call( this ); };	OO.inheritClass( dialogs.ActionChooseDialog, OO.ui.ProcessDialog ); OO.mixinClass( dialogs.ActionChooseDialog, PrimaryActionOnEnterMixin );

// Specify a name for .addWindows dialogs.ActionChooseDialog.static.name = 'ActionChooseDialog'; // Specify a title statically (or, alternatively, with data passed to the opening method). dialogs.ActionChooseDialog.static.title = 'Choose maintenance action';

dialogs.ActionChooseDialog.static.actions = [ {			action: 'save', label: 'Done', flags: [ 'primary' ], modes: [ 'can_execute' ] },		{			label: 'Cancel', flags: [ 'safe' ], modes: [ 'executing', 'can_execute' ] }	];

dialogs.ActionChooseDialog.prototype.addFieldFromNeed = function ( need ) { var input; // most widgets can use this var valuefunc = function ( i ) { return i.getValue; };

var eventName;

switch ( need.type ) { case 'page': input = new PageLookupTextInputWidget( {					namespaces: need.namespaces,					user: mw.config.get( 'wgUserName' )					// placeholder: "Index:Filename.djvu"				} ); break; case 'text': input = new OO.ui.TextInputWidget; break; case 'number': input = new OO.ui.NumberInputWidget( {					label: need.label,					value: need.value,					min: need.min,					max: need.max				} ); break; case 'bool': input = new OO.ui.ToggleSwitchWidget( {					label: need.label,					value: need.value				} ); // normal valuefunc break; case 'choice': input = new OO.ui.DropdownWidget( {					label: need.label,					menu: {						items: need.options.map( function ( c ) { return new OO.ui.MenuOptionWidget( {								data: c.data,								label: c.label							} ); } )					}				} );				valuefunc = function ( i ) { return i.getMenu.findSelectedItem.getData; };				break; case 'radio-button': var items = need.options.map( function ( c ) {					return new OO.ui.ButtonOptionWidget( { data: c.data, label: c.label } );				} );

input = new OO.ui.ButtonSelectWidget( {					items: items				} );

valuefunc = function ( i ) { return i.findSelectedItem.getData; };

eventName = 'choose';

break; case 'contrib': input = new ContribLookupTextInputWidget( {					user: need.user				} ); break; case 'wikitext-line': input = new WikitextLineLookup( {					filters: need.filters,					page: need.page || mw.config.get( 'wgPageName' )				} ); break; default: console.error( 'Unknown type ' + need.type ); }

if ( input ) { this.inputs.push( {				widget: input,				valuefunc: valuefunc			} ); var fl = new OO.ui.FieldLayout( input, {				label: need.label,				help: need.help,				align: 'top'			} );

var dialog = this;

// if the need is submit=true if ( need.submit && eventName ) { input.on( eventName, function {					dialog.executeAction( 'save' );				} ); }

// disable help tabbing, which gets in the way a LOT fl.$element.find( '.oo-ui-fieldLayout-help a' ) .attr( 'tabindex', '-1' );

this.param_fieldset.addItems( [ fl ] ); }	};

// Customize the initialize function: This is where to add content // to the dialog body and set up event handlers. dialogs.ActionChooseDialog.prototype.initialize = function { // Call the parent method. dialogs.ActionChooseDialog.super.prototype.initialize.call( this ); // Create and append a layout and some content. this.fieldset = new OO.ui.FieldsetLayout( {			label: 'Please choose an action',			classes: [ 'userjs-maintain_fs' ]		} );

this.$body.append( this.fieldset.$element );

var dialog = this;

this.buttonGroup = new OO.ui.ButtonSelectWidget( {			items: []		} ); this.fieldset.addItems( [ this.buttonGroup ] );

this.inputs = []; this.param_fieldset = new OO.ui.FieldsetLayout( {			label: 'Action parameters',			classes: [ 'userjs-maintain_fs' ]		} ); this.$body.append( this.param_fieldset.$element );

this.param_fieldset.toggle( false );

this.buttonGroup.on( 'select', function ( e ) {

for ( var i = 0; i < dialog.inputs.length; ++i ) { dialog.param_fieldset.clearItems; }			dialog.inputs = [];

if ( !e ) { return; // unselection }

if ( e.data.needs ) {

for ( var n = 0; n < e.data.needs.length; ++n ) { var need = e.data.needs[ n ];

dialog.addFieldFromNeed( need ); }

dialog.param_fieldset.toggle( dialog.inputs.length > 0 ); } else { // immediate dialog.executeAction( 'save' ); }		} );	};

dialogs.ActionChooseDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action === 'save' ) { return new OO.ui.Process( function {				var btn = dialog.buttonGroup.findSelectedItem;

var params = [];

for ( var i = 0; i < dialog.inputs.length; ++i ) { params.push( dialog.inputs[ i ].valuefunc( dialog.inputs[ i ].widget ) ); }

if ( !btn ) { OO.ui.alert( 'Please choose an action.' ); } else { dialog.actions.setMode( 'executing' ); var accepted_promise = dialog.saveCallback( btn.data, params ); // close the dialog if the user accepted the edit // and the edit succeeded accepted_promise .then( function {							console.log( 'Accepted' );							dialog.close;						}, function  {							console.log( 'Not accepted/failed' );							dialog.buttonGroup.unselectItem;							dialog.actions.setMode( 'can_execute' );						} ); }			} );		} else {			return new OO.ui.Process( function { dialog.cancelCallback; dialog.close; } );		}	};

// Use getSetupProcess to set up the window with data passed to it at the time // of opening (e.g., url: 'http://www.mediawiki.org', in this example). dialogs.ActionChooseDialog.prototype.getSetupProcess = function ( data ) { data = data || {}; return dialogs.ActionChooseDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {				var dialog = this;

var add_button = function ( opts ) { var btn = new OO.ui.ButtonOptionWidget( {						label: opts.label,						title: opts.help,						data: opts					} );

dialog.buttonGroup.addItems( [ btn ] ); };

// Set up contents based on data

for ( var i = 0; i < data.tools.length; ++i ) { var tool = data.tools[ i ]; add_button( tool ); }

this.saveCallback = data.saveCallback; this.cancelCallback = data.cancelCallback;

this.actions.setMode( 'can_execute' );

}, this );	};

dialogs.ActionChooseDialog.prototype.getBodyHeight = function { // Note that "expanded: false" must be set in the panel's configuration for this to work. // When working with a stack layout, you can use: //  return this.panels.getCurrentItem.$element.outerHeight( true ); return 300; };

/* */

function DiffConfirmDialog( config ) { DiffConfirmDialog.super.call( this, config ); }	OO.inheritClass( DiffConfirmDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows DiffConfirmDialog.static.name = 'diffConfirmDialog'; // Specify a title statically (or, alternatively, with data passed to the opening method). DiffConfirmDialog.static.title = 'Confirm change';

DiffConfirmDialog.static.actions = [ { action: 'save', label: 'Done', flags: 'primary' }, { label: 'Cancel', flags: 'safe' } ];

// Customize the initialize function: This is where to add content to // the dialog body and set up event handlers. DiffConfirmDialog.prototype.initialize = function { // Call the parent method. DiffConfirmDialog.super.prototype.initialize.call( this ); // Create and append a layout and some content. this.content = new OO.ui.PanelLayout( {			padded: true,			expanded: false		} );

this.$pageTitleElem = $( ' ' );

$( ' ' )			.append( 'Page: ', this.$pageTitleElem ) .appendTo( this.content.$element );

$( ' ' )			.append( 'Please confirm the changes:' ) .appendTo( this.content.$element );

this.$body.append( this.content.$element );

this.summary = new OO.ui.TextInputWidget( {			placeholder: 'Summary'		} );

this.content.$element.append( this.summary.$element ); };

DiffConfirmDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( !this.no_changes && action === 'save' ) { return new OO.ui.Process( function {				dialog.saveCallback( { summary: dialog.summary.getValue } );				dialog.close;			} ); } else { return new OO.ui.Process( function {				dialog.cancelCallback;				dialog.close;			} ); }	};

// Use getSetupProcess to set up the window with data passed to it at the time // of opening (e.g., url: 'http://www.mediawiki.org', in this example). DiffConfirmDialog.prototype.getSetupProcess = function ( data ) { data = data || {}; var dialog = this; return DiffConfirmDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {				// Set up contents based on data				dialog.saveCallback = data.saveCallback;				dialog.cancelCallback = data.cancelCallback;				dialog.pageTitle = data.pageTitle;

dialog.$pageTitleElem .empty .append( $( '' )						.attr( 'href', mw.config.get( 'wgArticlePath' ).replace( '$1', dialog.pageTitle ) )						.append( dialog.pageTitle )					);

var shortened = inductiveload.difference.shortenDiffString( data.diff, 100 ).join( ' ' );

if ( shortened && shortened.length ) { this.content.$element.append( "" + shortened + ' ' ); this.no_changes = false; } else { this.content.$element.append( ' ', new OO.ui.MessageWidget( { type: 'notice', inline: true, label: 'No changes made.' } ).$element ); this.no_changes = true; }

dialog.summary.setValue( data.summary );

}, this );	};

/* */	function RegexTextInputWidget( config ) { // Configuration initialization config = $.extend( {			validate: this.validate,			getCaseSensitivity: function { return true; }		}, config ); // Parent constructor RegexTextInputWidget.super.call( this, config ); // Properties this.text = null; this.getCaseSensitivity = config.getCaseSensitivity; this.getUseRegex = config.getUseRegex; // Events this.connect( this, {			change: 'onChange'		} ); // Initialization this.setWorkingText( '' ); }	OO.inheritClass( RegexTextInputWidget, OO.ui.ComboBoxInputWidget );

RegexTextInputWidget.prototype.setWorkingText = function ( text ) { this.text = text; this.emit( 'change' ); };

RegexTextInputWidget.prototype.escapeRegex = function ( string ) { return string.replace( /[-/\\^$*+?.|[\]{}]/g, '\\$&' ); };

RegexTextInputWidget.prototype.makeRegexp = function ( value ) {

if ( !value ) { value = this.getValue; }

if ( !this.getUseRegex ) { value = this.escapeRegex( value ); }

var flags = 'g';

if ( !this.getCaseSensitivity ) { flags += 'i'; }

return new RegExp( value, flags ); };

RegexTextInputWidget.prototype.validate = function ( value ) {

if ( !this.getUseRegex ) { return true; }

try { this.makeRegexp( value ); } catch ( e ) { return false; }		return true; };

RegexTextInputWidget.prototype.onChange = function ( value ) { var label; if ( !value ) { value = this.getValue; }		if ( !this.text ) { label = 'loading'; } else if ( !value ) { label = ''; } else { try { var regex = this.makeRegexp( value ); var matches = this.text.match( regex ); var count = matches ? matches.length : 0;

var suff = ( count === 1 ) ? 'match' : 'matches'; label = String( count ) + ' ' + suff; } catch ( e ) { label = 'bad regex'; }		}

this.setLabel( label ); };	/* */	function AutocompleteController( entries ) { this.maxEntries = 100; this.entries = entries; }

AutocompleteController.prototype.addEntry = function ( content ) { // trim list and filter in-place this.entries.splice( 0, this.maxEntries - 1, ...this.entries.filter( ( e ) => { return e.content !== content; } ) );

this.entries.push( {			content: content		} ); };

/* */

function ReplaceDialog( config ) { ReplaceDialog.super.call( this, config ); PrimaryActionOnEnterMixin.call( this );

this.storageId = 'gadget-maintain-replace-config';

this.config = mw.storage.getObject( this.storageId ) || { patterns: [], replacements: [], isRegex: true, caseSensitive: true, version: 1 };

this.autocompletes = { patterns: new AutocompleteController( this.config.patterns ), replacements: new AutocompleteController( this.config.replacements ) };	}	OO.inheritClass( ReplaceDialog, OO.ui.ProcessDialog ); OO.mixinClass( ReplaceDialog, PrimaryActionOnEnterMixin );

// Specify a name for .addWindows ReplaceDialog.static.name = 'replaceDialog'; // Specify a title statically (or, alternatively, with data passed to the opening method). ReplaceDialog.static.title = 'Replace in page';

ReplaceDialog.static.actions = [ { action: 'save', label: 'Done', flags: 'primary' }, { label: 'Cancel', flags: 'safe' } ];

ReplaceDialog.prototype.storeConfig = function { mw.storage.setObject( this.storageId, this.config ); };

// Customize the initialize function: This is where to add content to // the dialog body and set up event handlers. ReplaceDialog.prototype.initialize = function { // Call the parent method. ReplaceDialog.super.prototype.initialize.call( this ); var dialog = this;

// Create and append a layout and some content. this.fieldset = new OO.ui.FieldsetLayout( {			label: 'Replacement set-up',			classes: [ 'userjs-maintain_fs' ]		} );

// Add the FieldsetLayout to a FormLayout. var form = new OO.ui.FormLayout( {			items: [ this.fieldset ]		} );

this.$body.append( form.$element );

this.inputs = {};

this.inputs.case_sensitive = new OO.ui.ToggleSwitchWidget( {			help: 'Case sensitive search',			value: true		} ); this.inputs.use_regex = new OO.ui.ToggleSwitchWidget( {			help: 'Regular expression search',			value: true		} ); this.inputs.search = new RegexTextInputWidget( {			placeholder: 'Search pattern',			name: 'search-pattern',			getCaseSensitivity: function {				return dialog.inputs.case_sensitive.getValue;			},			getUseRegex: function  {				return dialog.inputs.use_regex.getValue;			},			options: this.config.patterns.map( ( r ) => { return { data: r.content, label: r.content };			} ),			menu: {				filterFromInput: true,				filterMode: 'substring'			}		} ); this.inputs.replace = new OO.ui.ComboBoxInputWidget( {			placeholder: 'Replacement pattern',			name: 'replace-pattern',			options: this.config.replacements.map( ( r ) => { return { data: r.content, label: r.content };			} ),			menu: {				filterFromInput: true,				filterMode: 'substring'			}		} );

this.inputs.case_sensitive.on( 'change', function {			dialog.inputs.search.emit( 'change' );		} ); this.inputs.use_regex.on( 'change', function {			dialog.inputs.search.emit( 'change' );		} );

this.fieldset.addItems( [			new OO.ui.FieldLayout( this.inputs.search, { label: 'Search for', align: 'right', help: 'Enter the search pattern as a JS regex (without slashes). E.g. Chapter \\d+ to match chapter headings.' } ),			new OO.ui.FieldLayout( this.inputs.replace, { label: 'Replacement', align: 'right', help: 'Use $1, $2, etc. for captured groups.' } ),			new OO.ui.FieldLayout( this.inputs.case_sensitive, { label: 'Case sensitive', align: 'right' } ),			new OO.ui.FieldLayout( this.inputs.use_regex, { label: 'Regular expression', align: 'right', help: 'Use a regular expression replacement pattern, rather than plain text.' } )		] );

// disable help tabbing, which gets in the way a LOT this.fieldset.$element.find( '.oo-ui-fieldLayout-help a' ) .attr( 'tabindex', '-1' ); };

ReplaceDialog.prototype.getSetupProcess = function ( data ) { data = data || {}; var dialog = this; return ReplaceDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {				// Set up contents based on data				this.saveCallback = data.saveCallback;				this.cancelCallback = data.cancelCallback;				this.pageTitle = data.pageTitle;

if ( data.selection ) { this.inputs.search.setValue( data.selection ); this.inputs.replace.setValue( data.selection ); }

// get and cache the current wikitext get_page_wikitext( this.pageTitle ) .done( function ( wikitext ) {						dialog.wikitext = wikitext;						dialog.inputs.search.setWorkingText( wikitext );					} ); }, this );	};

ReplaceDialog.prototype.getActionProcess = function ( action ) {

var unescapeBackslashes = function ( s ) { return s.replace( /\\n/g, '\n' ); };

var dialog = this; if ( !this.no_changes && action === 'save' ) { const patternString = dialog.inputs.search.getValue; const regex = dialog.inputs.search.makeRegexp( null ); const repl = dialog.inputs.replace.getValue;

// store autocompletion strings this.autocompletes.patterns.addEntry( patternString ); this.autocompletes.replacements.addEntry( repl );

this.storeConfig;

return new OO.ui.Process( function {				var summary = 'Replaced: ';				if ( dialog.inputs.use_regex.getValue ) {					summary += regex + ' → ' + repl;				} else {					summary += patternString + ' → ' + repl;				}

var acceptedPromise = dialog.saveCallback( {					regex: regex,					replace: unescapeBackslashes( repl ),					summary: summary				} );

// close the dialog if the user accepted the edit // and the edit succeeded acceptedPromise .then( function {						dialog.close;					} ); } );		} else {			return new OO.ui.Process( function { dialog.close; } );		}	};

/* */

var Maintain = { windowManager: undefined, activated: false, reloadOnChange: true, defaultTags: [ 'maintain.js' ], tools: [], /* list of filters that match a tool and mean it doesn't need confirming */ noconfirm_tools: [], signature: 'maintain_replace' };

mw.messages.set( {		'maintainjs-name': 'maintain.js',		'maintainjs-docpage': 'User:Inductiveload/maintain'	} );

function getSummarySuffix { return '(' + mw.msg( 'maintainjs-name' ) + ')'; }

function editPageApi( pageTitle, transformFunction, confirm, minor ) { console.log( `Making API edit on ${pageTitle}` );

var api = new mw.Api; var revid = mw.config.get( 'wgRevisionId' ); var title = pageTitle;

var promise; if ( revid !== 0 ) {

// wrap the transform function to get the content out of a revision const revEditFunction = function ( revision ) { return transformFunction( revision.content, confirm ) .then( function ( transformed ) {						transformed.summary += ' ' + getSummarySuffix;

if ( minor !== undefined ) { transformed.minor = minor; }						return transformed; } );			};

promise = api.edit( title, revEditFunction ); } else {

// do the transform ourselves on an empty string var transform_promise = transformFunction( '', confirm );

promise = transform_promise .then( function ( transformed ) {					api.create( title, {							summary: transformed.summary + ' ' + getSummarySuffix },						transformed.text );				} );		}

promise .then( function {				console.log( 'Page updated!' );

if ( Maintain.reloadOnChange ) { location.reload; }			}, function ( e, more ) {

// console.log("Edit/create rejected");

if ( e.name === 'EditCancelledException' ) { // console.log(e.message); return Promise.reject( 'EditCancelledException' ); } else { OO.ui.alert(						more ? more.error.info : e, {							title: 'Edit failed'						} ); }			} )			.catch( function ( e ) {} );

return promise; }

function editPageTextbox( transform_function, confirm ) { console.log( 'Making textbox edit' );

// eslint-disable-next-line no-jquery/no-global-selector var $input = $( '#wpTextbox1' ); // eslint-disable-next-line no-jquery/no-global-selector var $summary = $( '#wpSummary' );

// do the transform ourselves on an empty string var transform_promise = transform_function( $input.val, confirm );

var promise = transform_promise .then( function ( transformed ) {				$input.val( transformed.text );

var new_summary = $summary.val; if ( new_summary ) { new_summary += '; '; }				new_summary += transformed.summary; $summary.val( new_summary + ' ' + getSummarySuffix ); },			function ( e ) {

// console.log("Edit/create rejected");

if ( e.name === 'EditCancelledException' ) { // console.log(e.message); return Promise.reject( 'EditCancelledException' ); }			} )			.catch( function ( e ) { console.error( e ); } );

return promise; }

/* */

/*	 * Edit the page with the given promise-returning function. *	 * Returns a promise that rejects if the promise does (for	 * example, if the user rejects the change when prompted) */	function editPage( pageTitle, transformFunction, confirm, minor ) {

let retProm; if ( [ 'edit', 'submit' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) { // editing page - text in textbox if ( pageTitle !== mw.config.get( 'wgPageName' ) ) { alert( `Cannot edit page ${pageTitle} in this mode.` ); }			retProm = editPageTextbox( transformFunction, confirm, minor ); } else { retProm = editPageApi( pageTitle, transformFunction, confirm, minor ); }		return retProm; }

function confirmChange( title, old_text, new_text, summary ) { var diff_html = inductiveload.difference.diffString( old_text, new_text, false );

// Make the window. var dialog = new DiffConfirmDialog( {			size: 'medium'		} );

// eslint-disable-next-line compat/compat var confirmPromise = new Promise( function ( resolve, reject ) {

// Create and append a window manager, which will open and close the window. var windowManager = new OO.ui.WindowManager; $( document.body ).append( windowManager.$element ); windowManager.addWindows( [ dialog ] ); windowManager.openWindow( dialog, {				diff: diff_html,				summary: summary,				pageTitle: title,				/* resolve the promise if we confirm */				saveCallback: function ( confirmed ) {					resolve( { // user provided a better summary summary: confirmed.summary } );				},				cancelCallback: function {					reject;				}			} ); } );

return confirmPromise; }

function EditCancelledException { this.message = 'Edit cancelled'; this.name = 'EditCancelledException'; }

/* Make a transform to hand to the edit API *	 * Returns a functions that transforms text * and returns promise that resolves if the change is accepted * and is rejected on error or if the user rejects it. *	 * Resolution: { *  text: text, *  summary: summary * }	 */	function getTransformAndConfirmFunction( title, selectionInfo, textTransform ) { return function ( old_text, confirm ) { old_text = old_text || '';

var transform_result = textTransform( old_text, selectionInfo );

// tranform failed! abort! if ( transform_result === null ) { return new Promise( function ( resolve, reject ) {					reject( 'Transform failed' );				} ); }

var ret = { text: transform_result.text, summary: transform_result.summary };

// if (Maintain.defaultTags) { //  ret['tags'] = Maintain.defaultTags.join("|"); // }

// return a promise that resolves directly if ( !confirm ) { return new Promise( function ( resolve, reject ) {					resolve( ret );				} ); }

// return a promise that resolves with the transform return confirmChange( title, old_text, transform_result.text, transform_result.summary ) .then( function ( confirmation ) {					// update in case user changed it					ret.summary = confirmation.summary;					return ret;				}, function {					// rejection					console.log( 'User rejected' );					return Promise.reject( new EditCancelledException );					// return null;				} ); };	}

function make_template( template, params, newlines ) { var add = '';

return add; }

/*	 * Wrap a template name in so it shows linked in edit * summaries. */	function linkify_template( s ) { s = s.replace( '{' + '{',  ).replace( '}}',  ); return '{' + '{' + '[' + '[Template:' + s + '|' + s + ']]}}'; }

/* Append text, skipping certain lines from the end */ function append_text( text, appended, skip_line_patts, add_line_after, add_line_before ) { var lines = text.split( /\n/ ); var line_i = lines.length - 1; var last_was_blank = false; while ( line_i > 0 ) { var line = lines[ line_i ]; var matched = false;

for ( var i = 0; i < skip_line_patts.length; ++i ) { if ( skip_line_patts[ i ].test( line ) ) { matched = true; break; }			}

if ( matched ) { last_was_blank = /^\s*$/.test( line ); --line_i; } else { break; }		}

if ( add_line_before ) { lines[ line_i ] += '\n'; }

lines[ line_i ] += '\n' + appended;

if ( !last_was_blank && add_line_after ) { lines[ line_i ] += '\n'; }

return lines.join( '\n' ); }

function prepend_xfrm( prefix, separation, summary ) { return function ( old_text ) { return { text: prefix + ( old_text ? separation : '' ) + old_text, summary: summary || ( "Prepended '" + prefix + "'" ) };		};	}

function append_xfrm( suffix, separation, summary ) { return function ( old_text ) { return { text: old_text + ( old_text ? separation : '' ) + suffix, summary: summary || ( 'Appended: ' + suffix ) };		};	}

function add_template_transform( append, template, params, config ) { config = config || {}; var add = make_template( template, params, config.params_newlines );

if ( config.sign ) { // eslint-disable-next-line no-useless-concat add += ' ~' + '~' + '~' + '~'; // don't replace in JS source }

var separation = config.separation || '\n';

var summary = config.summary || 'Add ' + linkify_template( template );

if ( append ) { return append_xfrm( add, separation, summary ); }

return prepend_xfrm( add, separation, summary ); }

function add_cat_xfrm( cat ) { // stop MW categorising the JS page... // eslint-disable-next-line no-useless-concat var cat_str =  + 'Category:' + cat + ;

return function ( old ) { var lines = old.split( /\n/ );

var line_i = lines.length - 1; while ( line_i > 0 && lines[ line_i ].trim.length === 0 ) { line_i--; }

var add_str = '\n' + cat_str; if ( !lines[ line_i ].trim.startsWith( '[[Category' ) ) {				add_str = '\n' + add_str;			}

return { text: old + add_str, summary: 'Add category: ' + cat_str };		};	}

/*	 * Takes a list of [regex, repl] pairs and applies * them in order. *	 * Returns the transform and the summary as an object */	function regexTransform( res, summary ) {

return function ( old ) { for ( var i = 0; i < res.length; ++i ) { old = old.replace( res[ i ][ 0 ], res[ i ][ 1 ] ); }			return { text: old, summary: summary };		};	}

/*	 * Takes a list of [needle, repl] pairs and applies * them in order. The pattern is a plain string and will match exactly. *	 * Returns the transform and the summary as an object */	function replaceTransform( res, summary ) {

const regexExscapePatt = /[-[\]/{}*+?.\\^$|]/g; const escapeRegex = function ( needle ) { return needle.replace( regexExscapePatt, '\\$&' ); };

const regexps = res.map( ( [ needle, repl ] ) => {			return [ new RegExp( escapeRegex( needle ) ), repl ];		} );

// defer to the generic regexp transform return regexTransform( regexps, summary ); }

function delete_templates_transform( templates, summary ) {

return function ( old ) { var removed = [];

for ( var i = 0; i < templates.length; ++i ) { // TODO parse properly to matching braces var re = new RegExp( '', 'i' ); var new_text = old.replace( re, '' );

if ( new_text !== old ) { removed.push( templates[ i ] ); old = new_text; }			}

if ( !summary ) { removed = removed.map( linkify_template ).join( ', ' ); summary = 'Removed templates: ' + removed; }

return { text: old, summary: summary };		};	}

function chain_transform( transforms, summary ) {

return function ( old ) { var summaries = []; for ( var i = 0; i < transforms.length; ++i ) { var res = transforms[ i ]( old );

if ( old !== res.text ) { if ( !summary ) { summaries.push( res.summary ); }					old = res.text; }			}			return { text: old, summary: summary || summaries.join( '; ' ) };		};	}

/* export the transforms */ inductiveload.maintain.transforms = { chain: chain_transform, regex: regexTransform, replace: replaceTransform, add_category: add_cat_xfrm, add_template: add_template_transform, delete_templates: delete_templates_transform, append: append_xfrm, prepend: prepend_xfrm };	inductiveload.maintain.utils = { make_template_str: make_template, linkify_template: linkify_template, append_text: append_text };

function generic_match( test, candidate ) { if ( typeof test === 'string' ) { if ( test === candidate ) { return true; }		} else if ( test instanceof RegExp ) { if ( test.test( candidate ) ) { return true; }		} else if ( test instanceof Function ) { if ( test( candidate ) ) { return true; }		}		return false; }

function generic_match_any( test_list, candidate ) { for ( var i = 0; i < test_list.length; ++i ) { if ( generic_match( test_list[ i ], candidate ) ) { return true; }		}		return false; }

function tool_needs_confirm( noconfirm_list, tool_id ) { return !generic_match_any( noconfirm_list, tool_id ); }

function apply_config( cfg ) { // add the config tools [].push.apply( Maintain.tools, cfg.tools );

[].push.apply( Maintain.noconfirm_tools, cfg.noconfirm_tools ); }

function init { if ( !Maintain.activated ) { Maintain.windowManager = new OO.ui.WindowManager; // Create and append a window manager, which will open and close the window. $( document.body ).append( Maintain.windowManager.$element );

var blank_cfg = { tools: [], noconfirm_tools: [] };			// user-provided configs mw.hook( Maintain.signature + '.config' ) .fire( inductiveload.maintain, blank_cfg );

apply_config( blank_cfg );

Maintain.activated = true; }	}

function getSelection { const selection = window.getSelection; let pageName;

if ( !selection.isCollapsed && selection.rangeCount > 0 ) { const $anchor = $( selection.anchorNode ); const $prpPageCont = $anchor.closest( '.prp-pages-output' );

if ( $prpPageCont.length > 0 ) { // the selection is inside a PRP section

// find the page marker before the selection const $pageMarkers = $prpPageCont.find( '.pagenum' ); const previous = [ ...$pageMarkers ].filter(					( elem ) => selection.anchorNode.compareDocumentPosition( elem ) ===							Node.DOCUMENT_POSITION_PRECEDING				).pop;

// pull the page name out of the data-page-name attribute pageName = previous.getAttribute( 'data-page-name' ); }		}

if ( !pageName ) { // use the current page pageName = mw.config.get( 'wgPageName' ); }

return { selection: selection.toString, pageTitle: pageName };	}

function activate {

init;

// Make the window. var dialog = new dialogs.ActionChooseDialog( {			size: 'medium'		} );

Maintain.windowManager.addWindows( [ dialog ] );

const selectionInfo = getSelection;

Maintain.windowManager.openWindow( dialog, {			tools: Maintain.tools,			selection: selectionInfo.selection,			pageTitle: selectionInfo.pageTitle,			/* resolve the promise if we confirm */			saveCallback: function ( tool, params ) {				var transform = tool.transform( params );				if ( transform ) {					const title = mw.config.get( 'wgPageName' );					// convert to an API transform and attempt a page edit					var transformFn = getTransformAndConfirmFunction( title, selectionInfo, transform );

var confirm = tool_needs_confirm( Maintain.noconfirm_tools, tool.id );

const minor = false; var editPromise = editPage( title, transformFn, confirm, minor );

return editPromise; } else { return new Promise( function ( resolve, reject ) {						reject( 'Transform failed.' );					} ); }			},			cancelCallback: function {} } );	}

function activateReplace {

init;

// Make the window. var dialog = new ReplaceDialog( {			size: 'medium'		} );

$( document.body ).append( Maintain.windowManager.$element ); Maintain.windowManager.addWindows( [ dialog ] );

var selectionInfo = getSelection;

Maintain.windowManager.openWindow( dialog, {			selection: selectionInfo.selection,			pageTitle: selectionInfo.pageTitle,			saveCallback: function ( repl_data ) {

const regex = repl_data.regex; const replc = repl_data.replace; const summary = repl_data.summary; const transform = regexTransform( [ [ regex, replc ] ],					summary );

const transform_fn = getTransformAndConfirmFunction(					selectionInfo.pageTitle, selectionInfo, transform );

const minor = true; const edit_prom = editPage( selectionInfo.pageTitle, transform_fn, true, minor );

// return the edit promise - if this fails, we ask again return edit_prom; }		} );	}

function installPortlet {

var portlet = mw.util.addPortletLink(			'p-tb',			'#',			'Maintenance',			't-maintenance',			'Maintenance tools'		);

$( portlet ).on( 'click', function ( e ) {			e.preventDefault;			activate;		} );

portlet = mw.util.addPortletLink(			'p-tb',			'#',			'Replace',			't-replace',			'Replace in page'		);

$( portlet ).on( 'click', function ( e ) {			e.preventDefault;			activateReplace;		} );

console.log( 'Portlet installed' ); }

function installCss( css ) { // eslint-disable-next-line no-jquery/no-global-selector $( 'head' ).append( '' + css + ' ' ); }

$( function {		installPortlet;

// Install CSS installCss( ` .userjs-maintain_fs {	padding: 0 16px;	margin-top: 12px !important; } .userjs-maintain-extra {	font-size: 85%; } .userjs-maintain-diff ins {	color: seagreen; } .userjs-maintain-diff del {	color: tomato; } ` ); } );

// eslint-disable-next-line no-undef }( jQuery, mediaWiki, OO ) );