User:Novem Linguae/Scripts/UserRightsDiff.js

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.
// <nowiki>

// === Compiled with Novem Linguae's publish.php script ======================

// === modules/UserRightsDiffHtmlProcessor.js ======================================================


class UserRightsDiffHtmlProcessor {
	constructor( $, mw ) {
		// eslint-disable-next-line no-jquery/variable-pattern
		this.$ = $;
		this.mw = mw;
	}

	execute() {
		// User:BradV/Scripts/SuperLinks.js
		this.onDomNodeInserted( 'mw-logevent-loglines', this.checkLog, this );

		// Special:UserRights, Special:Log, Special:Watchlist
		this.checkLog( this );
	}

	onDomNodeInserted( htmlClassString, fn, that ) {
		const observer = new MutationObserver( ( mutations ) => {
			mutations.forEach( ( mutation ) => {
				const htmlWasAdded = mutation.addedNodes.length;
				if ( htmlWasAdded ) {
					mutation.addedNodes.forEach( ( node ) => {
						if ( node.classList && node.classList.contains( htmlClassString ) ) {
							fn( that );
						}
					} );
				}
			} );
		} );
		const config = { childList: true, subtree: true };
		observer.observe( document.body, config );
	}

	checkLog( that ) {
		// don't run twice on the same page
		if ( that.$( '.user-rights-diff' ).length === 0 ) {
			// Special:UserRights, Special:Log, BradV SuperLinks
			that.$( '.mw-logevent-loglines .mw-logline-rights' ).each( function () {
				that.checkLine( this );
			} );
			// Special:Watchlist
			that.$( '.mw-changeslist-log-rights .mw-changeslist-log-entry' ).each( function () {
				that.checkLine( this );
			} );
		}
	}

	checkLine( el ) {
		let text = this.$( el ).text();
		// We're going to write this text back to the browser window later. HTML escape it to prevent XSS.
		text = this.mw.html.escape( text );
		const stringProcessor = new UserRightsDiffStringProcessor();
		let from, to;
		try {
			[ from, to ] = stringProcessor.logEntryStringToArrays( text );
		} catch ( err ) {
			throw new Error( 'UserRightsDiff.js error. Error was: ' + err + '. Input text was: ' + this.$( el ).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: ' + this.permArrayToString( added ) + ']</span>' :
			'';
		removed = removed.length > 0 ?
			'<span class="user-rights-diff" style="background-color:yellow">[REMOVED: ' + this.permArrayToString( removed ) + ']</span>' :
			'';
		const noChange = added.length === 0 && removed.length === 0 ?
			'<span class="user-rights-diff" style="background-color:lightgray">[NO CHANGE]</span>' :
			'';
		this.$( el ).append( `<br />${ added } ${ removed } ${ noChange }` );
	}

	permArrayToString( array ) {
		array = array.join( ', ' );
		return array;
	}
}


// === modules/UserRightsDiffStringProcessor.js ======================================================

class UserRightsDiffStringProcessor {

	/** Returns 2 arrays. For example: [ [ 'autopatrolled' ], [ 'sysop' ] ] */
	logEntryStringToArrays( text ) {
		const fromMatches = /revoked ([^;(]+)/.exec( text );
		const fromMatch = fromMatches && fromMatches[ 1 ];
		const from = this.permStringToArray( fromMatch );

		const toMatches = /granted ([^;(]+)/.exec( text );
		const toMatch = toMatches && toMatches[ 1 ];
		let to = this.permStringToArray( toMatch );

		const autoUpdatedMatches = /automatically updated .+ to ([a-z ]+)/.exec( text );
		const autoUpdateMatch = autoUpdatedMatches && autoUpdatedMatches[ 1 ];
		const autoUpdate = this.permStringToArray( autoUpdateMatch );
		// array_merge( to, autoUpdate )
		to = to.concat( autoUpdate.filter( ( x ) => !to.includes( x ) ) );

		return [ from, to ];
	}

	permStringToArray( string ) {
		if ( string === null ) {
			return [];
		}
		// trim(), to get the space between the last perm and a parenthesis
		string = string.trim();
		string = string.replace( /^(.*) and (.*?$)/, '$1, $2' );
		const array = string.split( ', ' );
		return array;
	}
}
$(async function() {

// === UserRightsDiff.js ======================================================

// 

/*

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

*/


$( () => {
	( new UserRightsDiffHtmlProcessor( $, mw ) ).execute();
} );

// 


});

// </nowiki>