User:Nw520/AutoReplace.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
/**
 * AutoReplace
 *
 * Maintainer: [[m:User:nw520]]
 */
/* eslint-disable es/no-object-entries */
/* eslint-disable es/no-object-fromentries */
/* eslint-disable no-restricted-properties */
/* eslint-disable no-restricted-syntax */
$.ready.then( function () {
	let dialog = null;
	let windowManager = null;
	/** @type {Array<Object>} */
	const replacementRules = window.nw520_config?.autoReplace_replacements ?? [];

	function main() {
		if ( mw.config.get( 'wgPageContentModel' ) !== 'wikitext' || new URL( window.location.href ).searchParams.get( 'undo' ) !== null || replacementRules === null ) {
			return;
		}

		const [ autoReplacements, manualReplacements ] = replacementRules.reduce( ( [ accAuto, accManual ], val ) => {
			if ( val.manual !== undefined ) {
				accManual.push( val );
			} else {
				accAuto.push( val );
			}
			return [ accAuto, accManual ];
		}, [ [], [] ] );

		// Execute auto rules
		const urlSearchParams = new URLSearchParams( window.location.search );
		if ( mw.config.get( 'wgAction' ) === 'edit' && ![ 'off', 'no' ].includes( urlSearchParams.get( 'autoreplace' )?.toLowerCase() ) ) {
			performReplacements( autoReplacements, true );
		}

		// Add action for manual rules
		if ( manualReplacements.length > 0 ) {
			addManualAction( manualReplacements );
		}
	}

	async function addManualAction( manualReplacements ) {
		await $.when( mw.loader.using( 'ext.wikiEditor' ), $.ready );
		const groups = manualReplacements.reduce( ( acc, rule ) => {
			if ( acc[ rule.manual ] === undefined ) {
				acc[ rule.manual ] = [];
			}

			acc[ rule.manual ].push( rule );
			return acc;
		}, {} );

		// eslint-disable-next-line no-jquery/no-global-selector
		$( '#wpTextbox1' ).wikiEditor( 'addToToolbar', {
			section: 'main',
			group: 'format',
			tools: {
				AutoReplace: {
					label: 'AutoReplace',
					type: 'button',
					icon: '//upload.wikimedia.org/wikipedia/commons/thumb/1/1c/Noun_Project_replace_icon_581723.svg/20px-Noun_Project_replace_icon_581723.svg.png',
					action: {
						type: 'callback',
						execute: async function ( context ) {
							const filteredGroups = Object.fromEntries( Object.entries( groups ).filter( ( [ groupName, groupRules ] ) => {
								const groupApplies = performReplacements( groups[ groupName ], false, true );
								return groupApplies;
							} ) );
							
							if ( Object.keys( filteredGroups ).length === 0 ) {
								mw.notify( 'Es gibt keine anwendbaren Ersetzungsregeln.' );
								return;
							}
							
							const selectedGroup = await manualWindow( filteredGroups );

							performReplacements( groups[ selectedGroup ], false );
						}
					}
				}
			}
		} );
	}

	/**
	 * @param {string} text
	 * @param {string} group
	 */
	function appendSummary( text, group ) {
		let summaryValue = document.forms.editform.wpSummary.value;
		const groups = {};

		// Parse
		let section = null;
		const sectionParts = summaryValue.match( /^\/\* (?<section>[^*]+) \*\/(?: *)(?<summaryValue>.*)$/ );
		if ( sectionParts !== null ) {
			section = sectionParts.groups.section;
			summaryValue = sectionParts.groups.summaryValue;
		}

		for ( let groupSegment of summaryValue.split( '; ' ).filter( ( segment ) => segment !== '' ) ) {
			let groupName;
			const groupParts = groupSegment.match( /^(?<group>.+?): (?<text>.+?)$/ );
			if ( groupParts === null ) {
				groupName = groupSegment;
				groupSegment = '';
			} else {
				groupName = groupParts.groups.group;
				groupSegment = groupParts.groups.text;
			}

			if ( groups[ groupName ] === undefined ) {
				groups[ groupName ] = [];
			}
			groups[ groupName ] = [ ...groups[ groupName ], ...groupSegment.split( ', ' ).filter( ( segment ) => segment !== '' ) ];
		}

		// Append
		if ( groups[ group ?? text ] === undefined ) {
			groups[ group ?? text ] = [];
		}
		if ( group !== null && !groups[ group ].includes( text ) ) {
			groups[ group ].push( text );
		}

		// Stringify
		const newSummary = `${section === null ? '' : `/* ${section} */ `}${Object.entries( groups ).map( ( [ groupName, groupValues ] ) => {
			const valueDelimiter = groupName === '' ? '; ' : ', ';

			if ( groupValues.length === 0 ) {
				return `${groupName}`;
			} else {
				return `${groupName}: ${groupValues.join( valueDelimiter )}`;
			}
		} ).join( '; ' )}`;

		document.forms.editform.wpSummary.value = newSummary;
	}

	function manualWindow( groups ) {
		return new Promise( ( resolve ) => {
			mw.loader.using( [ 'oojs-ui' ] ).then( () => {
				const AutoReplaceWindow = function ( config ) {
					AutoReplaceWindow.super.call( this, config );
					this.groups = groups;
				};
				OO.inheritClass( AutoReplaceWindow, OO.ui.ProcessDialog );

				AutoReplaceWindow.static.name = 'autoreplace';
				AutoReplaceWindow.static.title = 'AutoReplace';
				AutoReplaceWindow.static.actions = [
					{ action: 'close', label: 'Close', flags: [ 'safe', 'close' ] }
				];
				AutoReplaceWindow.prototype.getBodyHeight = function () {
					return 250;
				};
				AutoReplaceWindow.prototype.initialize = function () {
					AutoReplaceWindow.super.prototype.initialize.apply( this, arguments );
					this.content = new OO.ui.PanelLayout( { padded: true } );

					const div = document.createElement( 'div' );

					for ( const groupName of Object.keys( this.groups ) ) {
						const button = new OO.ui.ButtonWidget( {
							label: groupName
						} );
						button.$element[ 0 ].addEventListener( 'click', ( e ) => {
							e.preventDefault();
							resolve( groupName );
							this.close();
						} );

						div.insertAdjacentElement( 'beforeend', button.$element[ 0 ] );
					}

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

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

				/**
				 * @param {string} action
				 * @return {OO.ui.Process}
				 */
				AutoReplaceWindow.prototype.getActionProcess = function ( action ) {
					return AutoReplaceWindow.super.prototype.getActionProcess.call( this, action ).next( () => {
						const state = this.close( { action: action } );
						return state.closed;
					}, this );
				};

				// Create a window manager. Specify the name of the factory with the ‘factory’ config.
				if ( windowManager === null ) {
					windowManager = new OO.ui.WindowManager();
					$( document.body ).append( windowManager.$element );
				}

				dialog = new AutoReplaceWindow( groups );
				windowManager.addWindows( [ dialog ] );
				windowManager.openWindow( dialog );
			} );
		} );
	}

	function performReplacements( replacements, autoReplace = false, test = false ) {
		// eslint-disable-next-line no-jquery/no-global-selector
		const $textbox = $( '#wpTextbox1' );
		let madeChanges = false;
		let lockSave = false;

		// Apply replacements
		for ( const replaceRule of replacements ) {
			const maxIterations = replaceRule?.iterative === true ? Number.MAX_SAFE_INTEGER : ( replaceRule?.iterative ?? 1 );

			let changesMade = false;
			let justMadeChanges = false;
			let iterations = 0;
			let newText = $textbox.val();
			
			do {
				if ( iterations >= maxIterations ) {
					console.warn( '[AutoReplace] Rule reached iteration limit: ', replaceRule );
					break;
				}
				
				const newerText = newText.replace( replaceRule.find, replaceRule.replace );
				justMadeChanges = newText !== newerText;
				newText = newerText;

				changesMade = changesMade || justMadeChanges;
				
				iterations++;
			} while ( justMadeChanges && !test && maxIterations !== 1 );
			
			if ( changesMade ) {
				if ( test ) {
					return true;
				}
				
				$textbox.val( newText );
				madeChanges = true;
				lockSave = lockSave || ( replaceRule.trivial !== undefined ? !replaceRule.trivial : true );

				// Append to summary
				if ( replaceRule.comment !== undefined ) {
					const group = typeof replaceRule.comment === 'string' ? null : replaceRule.comment[ 0 ];
					const comment = typeof replaceRule.comment === 'string' ? replaceRule.comment : replaceRule.comment[ 1 ];

					appendSummary( comment, group );
				}
			}
		}

		if ( lockSave ) {
			// Lock save-button
			document.getElementById( 'wpSave' ).addEventListener( 'click', function ( e ) {
				if ( !e.ctrlKey ) {
					e.preventDefault();
					mw.notify( 'Es wurden automatische Änderungen durchgeführt. Bitte diese erst überprüfen.' );
				}
			} );
		}
		if ( madeChanges && ( window.nw520_config?.autoReplace_autoDiff === true || window.nw520_config?.autoReplace_autoDiff === 'auto' && autoReplace ) ) {
			document.getElementById( 'wpDiff' ).click();
		}
		
		return madeChanges;
	}

	window.AutoReplace = {
		performReplacements: performReplacements
	};

	main();
} );
// </nowiki>