User:Inductiveload/maintain.js
Jump to navigation
Jump to search
Note: After saving, changes may not occur immediately. Click here to learn how to bypass your browser's cache.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Cmd-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (Cmd-Shift-R on a Mac)
- Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Clear the cache in Tools → Preferences
For details and instructions about other browsers, see Wikipedia:Bypass your cache.
Code that you insert on this page could contain malicious content capable of compromising your account. If you are unsure whether code you are adding to this page is safe, you can ask at the central discussion page, Scriptorium. The code will be executed when previewing this page under some skins, including Monobook. You can in the interim if you wish to refresh the content sooner under another skin. |
This script seems to have a documentation page at User:Inductiveload/maintain. |
/**
* 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( "<p class='userjs-maintain-extra'>" +
edit.revid + ' (' + size + ', ' + time + ')</p>' )
];
if ( edit.comment ) {
content.push(
new OO.ui.HtmlSnippet( "<p class='userjs-maintain-extra'>" + edit.comment + '</p>' )
);
}
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 = $( '<span>' );
$( '<p>' )
.append( 'Page: ', this.$pageTitleElem )
.appendTo( this.content.$element );
$( '<p>' )
.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( $( '<a>' )
.attr( 'href', mw.config.get( 'wgArticlePath' ).replace( '$1', dialog.pageTitle ) )
.append( dialog.pageTitle )
);
var shortened = inductiveload.difference.shortenDiffString( data.diff, 100 ).join( '<hr />' );
if ( shortened && shortened.length ) {
this.content.$element.append( "<pre class='userjs-maintain-diff'>" + shortened + '</pre>' );
this.no_changes = false;
} else {
this.content.$element.append( '<br>', 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-docpage' ) + '|' + 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 = '{{' + template;
if ( params && params.length > 0 ) {
if ( newlines ) {
add += '\n | ';
} else {
add += '|';
}
add += params.join( newlines ? '|' : '\n | ' );
if ( newlines ) {
add += '\n';
}
}
add += '}}';
return add;
}
/*
* Wrap a template name in {{[[Template:%s|%s]]}} 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( '{{\\s*' + templates[ i ] + '(\\s*\\|.*?)?}}', '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( '<style type="text/css">' + css + '</style>' );
}
$( 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 ) );