MediaWiki: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.
/**
* 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.9 (2024-04-30)
*/
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:)?((?:Graph:)?[^#:}]+?)(\\}\\}|\\|)', '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() {
var value = this.input.query.value,
actualTarget = value.replace( prefix + '/', '' );
if ( capitalizeLinks ) {
actualTarget = actualTarget.substring( 0, 1 ).toUpperCase() + actualTarget.substring( 1 );
}
if (
!value.includes( prefix ) ||
!value.includes( actualTarget )
) {
this.input.query.setValue( prefix + '/' + actualTarget );
}
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' );
});
$( '.editor-overlay .header-cancel button' ).on( 'click touchend', function( e ) {
editor.text = removePrefixes( editor.text, prefix, capitalizeLinks );
});
} 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 ) {
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( 'ext.WikiEditor.realtimepreview.enable' ).add( function( preview ) {
var $textarea = preview.context.$textarea;
$textarea.data( 'jquery.textSelection' ).getContents = function() {
return addPrefixes( this.val(), prefix, capitalizeLinks, iwprefixes );
};
});
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' ) ) ||
!mw.config.get( 'wgIsProbablyEditable' )
) {
// 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 );
});
});
}
});
});