User:Jon Harald Søby/Gadget-AddPrefix.js

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.
mw.loader.load( '//incubator.wikimedia.org/w/index.php?title=MediaWiki:Globals.js&action=raw&ctype=text/javascript' );
mw.loader.load( "//incubator.wikimedia.org/w/index.php?title=MediaWiki:Gadget-EditorAPIs.js&action=raw&ctype=text/javascript" );
mw.loader.load( "//incubator.wikimedia.org/w/index.php?title=MediaWiki:Gadget-AddPrefix.css&action=raw&ctype=text/css", "text/css" );
mw.loader.using( [ 'mediawiki.api', 'mediawiki.jqueryMsg' ] ).then( function() {
/////////////////

/**
 * The AddPrefix gadget, v2.2
 * 
 * This gadget should make adding prefixes to links, templates and categories
 * a thing of the past. When a user loads the edit page, the gadget
 * _removes_ all prefixes, and leaves the page looking like it would in
 * a normal wiki. When the user saves the page, the prefixes are added back in
 * in the correct locations.
 * 
 * The gadget will only work on pages that are part of a test wiki (thus having
 * a prefix like Wx/xx), and is turned on automatically for pages in all normal
 * namespaces except Template and Module (where it would be important that a
 * script doesn't interfere with the raw code).
 * 
 * @author Jon Harald Søby
 * @version 2.2.3 (2022-12-01)
 */

function getIWPrefixes() {
	var res = new mw.Api().get( {
		action: 'query',
		format: 'json',
		formatversion: 2,
		meta: 'siteinfo',
		siprop: 'interwikimap'
	} ).then( function( data ) {
		var iwprefixes = [];
		for ( var iw in data.query.interwikimap ) {
			iwprefixes.push( data.query.interwikimap[ iw ].prefix );
		}
		return iwprefixes;
	} );
	return res;
}

function findUntouchables( content ) {
	// Code within <!-- comments --> or certain tags shouldn't be touched by
	// this script. This function finds such tags, and returns the content
	// with §§§§§ (five §s) instead of those tags, as well as all the matches
	// in an array.
	var regex = new RegExp( '(<!--.+?-->|< *(code|nowiki|syntaxhighlight)( [^>]+)?>.+?< *\\/ *\\2 *>)', 'gmis');
	var matches = content.match( regex ) || [];
	var splittablecontent = content.replace( regex, "§§§§§" );
	return [ splittablecontent, matches ];
}

function removePrefixes( content, prefix, capitalize ) {
	var untouchables = findUntouchables( content ),
		cleanContent = untouchables[ 0 ],
		comments = untouchables[ 1 ];

	var templateregex = new RegExp( '\\{\\{(#invoke:|Template:)?' + prefix + '/([^|}]+?)(\\||\\}\\})', 'gmis' );
	var linkregex = new RegExp( '\\[\\[\\|*(:?(Category|Template|Module|Talk)([_ ]talk)?:)?\\s*' + prefix + '/(.+?)(\\|$4|)\\]\\]', 'gmi' );
	var doublelinkregex = new RegExp( '\\[\\[(?!File:|Image:)(.+?)\\|(.+?)\\]\\]', 'gmi' );
	cleanContent = cleanContent.replace( templateregex, function( match, a, b, c ) {
		if ( /NAME|CURRENT|PAGE|NUMBER/.test( b ) ) {
			return match;
		} else {
			if ( !a ) a = '';
			return "{{" + a + b + c;
		}
	});
	cleanContent = cleanContent.replace( linkregex, '[[$1$4]]' );
	cleanContent = cleanContent.replace( doublelinkregex, function( match, a, b ) {
		if ( a == b ) {
			return '[[' + a + ']]';
		} else if ( capitalize && ( a.substring( 0, 1 ).toLowerCase() + a.substring( 1 ) == b )) {
			// Change [[Abc|abc]] to [[abc]]
			return '[[' + b + ']]';
		} else {
			return match;
		}
	});

	var ccSplit = cleanContent.split( '§§§§§' ),
		cc = ccSplit[ 0 ];
	for ( var i = 0; i < comments.length; i++ ) {
		cc = cc + comments[ i ] + ccSplit[ i + 1 ];
	}
	return cc;
}

function addPrefixes( content, prefix, capitalize, iwprefixes ) {
	var untouchables = findUntouchables( content ),
		cleanContent = untouchables[ 0 ],
		comments = untouchables[ 1 ];

	/* Some namespace names that are also interwiki prefixes, and thus need special handling */
	var specialnamespaces = [
		'Wikipedia',
		'Wiktionary',
		'Wikivoyage',
		'Wikibooks',
		'Wikiquote',
		'Wikinews'
	];
	var templateregex = new RegExp( '\\{\\{(#invoke:|Template:|subst:)?([^#:}]+?)(\\}\\}|\\|)', 'gmis' );
	var linkregex = new RegExp(
		'\\[\\[\\' +
		'|*' + // this part is only here to fix errors created by the old version of this gadget
		'(?!:?File:|:?Image:|:?Media:|:?Special:|:?User:|:?User talk:)(:?(Category|Help|Template|Module|Talk|' +
		specialnamespaces.join( '|' ) + 
		')([_ ]talk)?:)?\\s*([^|\\]]+?)(\\|.+?)?\\]\\]',
		'gmi' );
	var basecategoryregex = new RegExp( '\\[\\[ *Category *: *' + prefix + ' *(\\|.+?)?\\]\\]', 'i' );
	// Add prefix to {{templates}}
	cleanContent = cleanContent.replace( templateregex, function( match, a, b, c ) {
		// match = The full match
		// a = Any prefix (Template:, subst: or #invoke:)
		// b = The template name
		// c = What ends the template line, i.e. "|" or "}}"
		if ( !a ) a = '';
		if ( capitalize ) {
			b = b.trimStart().substring( 0, 1 ).toUpperCase() + b.trimStart().substring( 1 );
		}
		if (
			/^([Dd]elete|!)$/.test( b.trim() ) ||
			b.trim() == 'INTERWIKI' ||
			/\{/.test( b ) ||
			( b.length > 5 && b == b.toUpperCase() && b.toLowerCase() != b.toUpperCase() )
		) {
			// [[Template:Delete]] and [[Template:INTERWIKI]] are special,
			// and can be used in any test project.
			//
			// If a template name is longer than 5 characters and all-uppercase,
			// it is probably a magic word, so leave it as it is. Also check
			// if lower-case is equal to upper-case, to avoid problems with scripts
			// that don't have casing.
			return match;
		} else {
			return '{{' + a + prefix + '/' + b + c;
		}
	});
	// Add prefix to [[links]]
	cleanContent = cleanContent.replace( linkregex, function( match, a, b, c, d, e ) {
		// match = The full match
		// a = Namespace name (if defined in the regex above), including colon
		// b = (not used, part of a)
		// c = (not used, part of a)
		// d = The target page name
		// e = Any text between | and ]] (including the |)
		if ( !a ) a = ''; // Set a to an empty string instead of "undefined"
		var target = d.replace( /^:/, '' );
		if ( capitalize ) {
			target = target.substring( 0, 1 ).toUpperCase() + target.substring( 1 );
		}
		var specialnamespaceregex = new RegExp( '(' + specialnamespaces.join( '|' ) + ')(?![ _]talk)', 'i' );
		if (
			!a && d.includes( ':' ) && iwprefixes.includes( target.split( ':' )[ 0 ].toLowerCase() ) ||
			/^[Ww][bnpqty]\/[a-z]{2,3}(-[a-z]+)*(\/|$)/.test( d )
		) {
			// If there is a colon in the link text (d) and it doesn't match
			// a namespace name (a), and it matches a prefix in the interwiki
			// prefix map, do nothing with the link.
			//
			// Also leave the link alone if it links to a different test wiki
			// than the one we are on (if we don't, it would result the link
			// getting two prefixes, so [[Wp/yy/Abc]] would be turned into
			// [[Wp/xx/Wp/yy/Abc]]).
			return match;
		} else if ( specialnamespaceregex.test( a ) ) {
			// If the link is to any of these namespaces, the namespace name
			// should go _after_ the prefix, not before.
			if ( !e ) e = '|' + a + target;
			return '[[' + prefix + '/' + a + target + e + ']]';
		} else if ( e || /[Cc]ategory\s*:$/.test( a ) ) {
			if ( !e ) e = '';
			if ( target == prefix ) {
				// Don't change anything for [[Category:Wx/xx]]
				return match;
			} else {
				return '[[' + a + prefix + '/' + target + e + ']]';
			}
		} else {
			return '[[' + a + prefix + '/' + target + '|' + d.replace( /^:/, '' ) + ']]';
		}
	});
	// Replace double instances of the prefix (for example if the user added it
	// manually while editing) with just a single instance.
	cleanContent = cleanContent.replace( new RegExp( '(' + prefix + '/\\s*)+' + prefix, 'gi' ), prefix );
	
	// Add the base category for the test wiki if it is not present
	var editingSection = ( mw.config.get( 'wgEditMessage' ) === 'editingsection' ) || /\/editor\/\d+$/.test( window.location.href );
	if ( !editingSection && mw.config.get( 'wgNamespaceNumber' ) === 0 && !basecategoryregex.test( cleanContent ) && !/^#REDIRECT/i.test( cleanContent ) ) {
		var categoryblockregex = /(\[\[ *[Cc]ategory *: *.+? *\]\]\n*)+(\n*\[\[ *[a-z]{2,3}(-[a-z]+)* *: *.+?\]\])*/g;
		var interwikiblockregex = /(\[\[ *[a-z]{2,3}(-[a-z]+)* *: *.+?\]\]\n*)+$/g;
		var categorymatches = cleanContent.match( categoryblockregex );
		var interwikimatches = cleanContent.match( interwikiblockregex );
		if ( categorymatches ) {
			cleanContent = cleanContent.replace( categorymatches[ categorymatches.length - 1 ], '[[Category:' + prefix + ']]\n' + categorymatches[ categorymatches.length - 1 ] );
		} else if ( interwikimatches ) {
			cleanContent = cleanContent.replace( interwikimatches[ 0 ], '[[Category:' + prefix + ']]\n\n' + interwikimatches[ 0 ] );
		} else {
			cleanContent = cleanContent.trimEnd() + '\n\n[[Category:' + prefix + ']]';
		}
	}

	var ccSplit = cleanContent.split( '§§§§§' ),
		cc = ccSplit[ 0 ];
	for ( var i = 0; i < comments.length; i++ ) {
		cc = cc + comments[ i ] + ccSplit[ i + 1 ];
	}
	return cc;
	
}

function init( editor, iwprefixes ) {
	var conf = mw.config.get( [
		'wgWmincTestwikiPrefix',
		'wgWmincTestwikiProject',
		'wgWmincTestwikiLanguage',
		'wgWmincLowercaseLanguages'
		] );
	var prefixhidden = false;
	
	var prefix = conf.wgWmincTestwikiPrefix;
	
	var capitalizeLinks = true;
	if ( conf.wgWmincTestwikiProject === 't' || conf.wgWmincLowercaseLanguages.includes( conf.wgWmincTestwikiLanguage ) ) {
		capitalizeLinks = false;
	}
	
	if ( editor.mode === 'visual' ) {
		mw.hook( 've.activationComplete' ).add( function() {
			// Modify Visual Editor's Link Annotation widget to include the
			// prefix automatically when creating or editing a link.
			var originalGetTextInputWidget = ve.ui.MWInternalLinkAnnotationWidget.prototype.getTextInputWidget;
			ve.ui.MWInternalLinkAnnotationWidget.prototype.getTextInputWidget = function() {
				if ( !this.input.query.value.includes( prefix ) ) {
					this.input.query.setValue( prefix + '/' + this.input.query.value );
				}
				return originalGetTextInputWidget.apply( this, arguments );
			};
		});
	} else {
		editor.text = removePrefixes( editor.text, prefix, capitalizeLinks );
		prefixhidden = true;
		if ( editor.mode === 'wikitext2017' ) {
			// Some times all text in the 2017 editor will be selected after
			// the initial prefix replacement. This avoids that.
			editor.selectionStart = 0;
			editor.selectionEnd = 0;
		}
		
		if ( editor.mode === 'mobileFrontend' ) {
			$( 'button.continue' ).on( 'click touchend', function( e ) {
				editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
				$( '#wikitext-editor' ).trigger( 'input' );
			});
		} else {
			// Add prefixes back in if you switch to Visual Editor
			mw.hook( 've.activationStart' ).add( function() {
				editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
				prefixhidden = false;
			});
			
			mw.hook( 'wikiEditor.toolbarReady' ).add( function ( $textarea ) {
				function removeTranslateTags( message ) {
					message = message.replaceAll( /((<|&lt;)\/?translate(>|&gt;)|(<|&lt;)!--T:\d--(>|&gt;)\s*)/g, '' );
					return message;
				}
				var helpmessage = mw.msg( 'wminc-gadget-addprefix-help' );
				var togglemessage = mw.msg( 'wminc-gadget-addprefix-toggle' );
				$textarea.wikiEditor( 'addToToolbar', {
					sections: {
						prefix: {
							type: 'toolbar',
							label: prefix + '/',
							groups: {
								prefixing: {
									label: helpmessage,
									tools: {
										togglePrefix: {
											label: togglemessage,
											type: 'button',
											oouiIcon: 'eye',
											action: {
												type: 'callback',
												execute: function( context ) {
													if ( prefixhidden ) {
														editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
														$( '.wikiEditor-ui-top .tab-prefix' ).removeClass( 'tab-prefix-active' );
														$( '.wikiEditor-ui-top span[rel=togglePrefix] .oo-ui-iconElement-icon' ).addClass( 'oo-ui-icon-eyeClosed' );
														prefixhidden = false;
													} else {
														editor.text = removePrefixes( editor.text, prefix, capitalizeLinks );
														$( '.wikiEditor-ui-top .tab-prefix' ).addClass( 'tab-prefix-active' );
														$( '.wikiEditor-ui-top span[rel=togglePrefix] .oo-ui-iconElement-icon' ).removeClass( 'oo-ui-icon-eyeClosed' );
														prefixhidden = true;
													}
												}
											}
										}
									}
								}
							}
						}
					}
				});
				$( '.wikiEditor-ui-top .tab-prefix' ).addClass( 'tab-prefix-active' );
			});
			
			mw.hook( 've.wikitextInteractive' ).add( function() {
				var originalSwitch = ve.ui.MWEditModeVisualTool.prototype.switch;
				ve.ui.MWEditModeVisualTool.prototype.switch = function () {
				    editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
				    prefixhidden = false;
				    return originalSwitch.apply( this, arguments );
				};
				mw.hook( 've.saveDialog.stateChanged' ).add( function() {
					editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
					prefixhidden = false;
					ve.init.target.saveDialog.on( 'close', function() {
					  editor.text = removePrefixes( editor.text, prefix, capitalizeLinks );
					  prefixhidden = true;
					});
				});
			});
			
			if ( mw.user.options.get( 'uselivepreview' ) ) {
				$( '#wpPreview, #wpDiff' ).on( 'click', function() {
					editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
				});
				mw.hook( 'wikipage.content' ).add( function() {
					editor.text = removePrefixes( editor.text, prefix, capitalizeLinks );
				});
			}
			
			$( '#editform' ).on( 'submit', function() {
				editor.text = addPrefixes( editor.text, prefix, capitalizeLinks, iwprefixes );
			});
		}
	}
}

mw.hook( 'incubator.globalsReady' ).add( function() {
	mw.hook( 'editorapi.ready' ).add( function( editor ) {
		var isIE = window.document.documentMode ? true : false;
		if (
			isIE ||
			!mw.config.get( 'wgWmincTestwikiPrefix' ) ||
			mw.config.get( 'wgWmincTestwikiPrefix' ) === mw.config.get( 'wgTitle' ) ||
			[ 8, 10, 828 ].includes( mw.config.get( 'wgNamespaceNumber' ) )
		) {
			// Do nothing if you use Internet Explorer, or if you're not on
			// a prefixed page, or if you're on an info page or a template or
			// module page.
			return;
		} else {
			return new mw.Api().loadMessagesIfMissing( [
				'wminc-gadget-addprefix-help',
				'wminc-gadget-addprefix-toggle'
			]).then( function() {
				getIWPrefixes().then( function( iwprefixes ) {
					init( editor, iwprefixes );
				});
			});
		}
	});
});


/////////////////
});