MediaWiki:Gadget-script-installer-core.js

( function {	// An mw.Api object	let api;

// Keep "common" at beginning const SKINS = [ 'common', 'monobook', 'minerva', 'vector', 'vector-2022', 'timeless' ];

// How many scripts do we need before we show the quick filter? const NUM_SCRIPTS_FOR_SEARCH = 5;

// The primary import list, keyed by target. (A "target" is a user JS subpage	// where the script is imported, like "common" or "vector".) Set in buildImportList const imports = {};

// Local scripts, keyed on name; value will be the target. Set in buildImportList. const localScriptsByName = {};

// How many scripts are installed? let scriptCount = 0;

// Goes on the end of edit summaries const ADVERT = ' (script-installer)';

/**	 * Strings, for translation */	const STRINGS = { skinCommon: 'common (applies to all skins)', backlink: 'Backlink:', installSummary: 'Installing $1', installLinkText: 'Install', installProgressMsg: 'Installing...', uninstallSummary: 'Uninstalling $1', uninstallLinkText: 'Uninstall', uninstallProgressMsg: 'Uninstalling...', disableSummary: 'Disabling $1', disableLinkText: 'Disable', disableProgressMsg: 'Disabling...', enableSummary: 'Enabling $1', enableLinkText: 'Enable', enableProgressMsg: 'Enabling...', moveLinkText: 'Move', moveProgressMsg: 'Moving...', movePrompt: 'Destination? Enter one of:', // followed by the names of skins normalizeSummary: 'Normalizing script installs', remoteUrlDesc: '$1, loaded from $2', panelHeader: 'You currently have the following scripts installed (find more at WP:USL)', cannotInstall: 'Cannot install', cannotInstallSkin: 'This page is one of your user customization pages, and may (will, if common.js) already run on each page load.', cannotInstallContentModel: "Page content model is $1, not 'javascript'", insecure: '(insecure)', // used at the end of some messages notJavaScript: 'not JavaScript', installViaPreferences: 'Install via preferences', showNormalizeLinks: 'Show "normalize" links?', normalize: 'normalize', showMoveLinks: 'Show "move" links?', quickFilter: 'Quick filter:', tempWarning: 'Installation of non-User, non-MediaWiki protected pages is temporary and may be removed in the future.', badPageError: 'Page is not User: or MediaWiki: and is unprotected', manageUserScripts: 'Manage user scripts', bigSecurityWarning: "Warning!$1\n\nAll user scripts could contain malicious content capable of compromising your account. Installing a script means it could be changed by others; make sure you trust its author. If you're unsure whether a script is safe, check at the technical village pump. Install this script?", securityWarningSection: ' About to install $1.' };

const USER_NAMESPACE_NAME = mw.config.get( 'wgFormattedNamespaces' )[ 2 ];

/**	 * Constructs an Import. An Import is a line in a JS file that imports a	 * user script. Properties: *	 * EXACTLY one of "page" or "url" are null for every Import. This * constructor should not be used directly; use the factory * functions (Import.ofLocal, Import.ofUrl, Import.fromJs) instead. *	 * this.type = 0 if local, 1 if remotely loaded, and 2 if URL. *	 * @param page a page name, such as "User:Foo/Bar.js". * @param wiki a wiki from which the script is loaded, such as	 *   "en.wikipedia" If null, the script is local, on the user's	 *    wiki. * @param url a URL that can be passed into mw.loader.load. * @param target the title of the user subpage where the script is, *   without the .js ending: for example, "common". * @param disabled whether this import is commented out. */	function Import( page, wiki, url, target, disabled ) { this.page = page; this.wiki = wiki; this.url = url; this.target = target; this.disabled = disabled; this.type = this.url ? 2 : ( this.wiki ? 1 : 0 ); }

Import.ofLocal = function ( page, target, disabled ) { if ( disabled === undefined ) { disabled = false; }		return new Import( page, null, null, target, disabled ); };

/**	 * URL to Import. Assumes wgScriptPath is "/w" */	Import.ofUrl = function ( url, target, disabled ) { if ( disabled === undefined ) { disabled = false; }		const URL_RGX = /^(?:https?:)?\/\/(.+?)\.org\/w\/index\.php\?.*?title=(.+?(?:&|$))/; const match = URL_RGX.exec( url ); if ( match ) { const title = decodeURIComponent( match[ 2 ].replace( /&$/, '' ) ), wiki = decodeURIComponent( match[ 1 ] ); return new Import( title, wiki, null, target, disabled ); }		return new Import( null, null, url, target, disabled ); };

Import.fromJs = function ( line, target ) { const IMPORT_RGX = /^\s*(\/\/)?\s*importScript\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/; let match = IMPORT_RGX.exec( line ); if ( match ) { return Import.ofLocal( unescapeForJsString( match[ 2 ] ), target, !!match[ 1 ] ); }

const LOADER_RGX = /^\s*(\/\/)?\s*mw\.loader\.load\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/; match = LOADER_RGX.exec( line ); if ( match ) { return Import.ofUrl( unescapeForJsString( match[ 2 ] ), target, !!match[ 1 ] ); }	};

Import.prototype.getDescription = function ( useWikitext ) { switch ( this.type ) { case 0: return useWikitext ? (  + this.page +  ) : this.page; case 1: return STRINGS.remoteUrlDesc.replace( '$1', this.page ).replace( '$2', this.wiki ); case 2: return this.url; }	};

/**	 * Human-readable (NOT necessarily suitable for ResourceLoader) URL. */	Import.prototype.getHumanUrl = function { switch ( this.type ) { case 0: return '/wiki/' + encodeURI( this.page ); case 1: return '//' + this.wiki + '.org/wiki/' + encodeURI( this.page ); case 2: return this.url; }	};

Import.prototype.toJs = function { const dis = this.disabled ? '//' : '';		let url = this.url; switch ( this.type ) { case 0: return dis + "importScript('" + escapeForJsString( this.page ) + "'); // " + STRINGS.backlink + ' ' + escapeForJsComment( this.page ) + ''; case 1: url = '//' + encodeURIComponent( this.wiki ) + '.org/w/index.php?title=' + encodeURIComponent( this.page ) + '&action=raw&ctype=text/javascript'; /* FALL THROUGH */ case 2: return dis + "mw.loader.load('" + escapeForJsString( url ) + "');"; }	};

/**	 * Installs the import. */	Import.prototype.install = function { return api.postWithEditToken( {			action: 'edit',			title: getFullTarget( this.target ),			summary: STRINGS.installSummary.replace( '$1', this.getDescription( /* useWikitext */ true ) ) + ADVERT,			appendtext: '\n' + this.toJs		} ); };

/**	 * Get all line numbers from the target page that mention * the specified script. */	Import.prototype.getLineNums = function ( targetWikitext ) { function quoted( s ) { return new RegExp( "(['\"])" + escapeForRegex( s ) + '\\1' );		}		let toFind;		switch ( this.type ) {			case 0:				toFind = quoted( escapeForJsString( this.page ) );				break;			case 1:				toFind = new RegExp( escapeForRegex( encodeURIComponent( this.wiki ) ) + '.*?' +                   escapeForRegex( encodeURIComponent( this.page ) ) );				break;			case 2:				toFind = quoted( escapeForJsString( this.url ) );				break;		}		const lineNums = [], lines = targetWikitext.split( '\n' );		for ( let i = 0; i < lines.length; i++ ) {			if ( toFind.test( lines[ i ] ) ) {				lineNums.push( i );			}		}		return lineNums;	};

/**	 * Uninstalls the given import. That is, delete all lines from the * target page that import the specified script. */	Import.prototype.uninstall = function { const that = this; return getWikitext( getFullTarget( this.target ) ).then( ( wikitext ) => {			const lineNums = that.getLineNums( wikitext ),				newWikitext = wikitext.split( '\n' ).filter( ( _, idx ) => lineNums.indexOf( idx ) < 0 ).join( '\n' );			return api.postWithEditToken( { action: 'edit', title: getFullTarget( that.target ), summary: STRINGS.uninstallSummary.replace( '$1', that.getDescription( /* useWikitext */ true ) ) + ADVERT, text: newWikitext } );		} );	};

/**	 * Sets whether the given import is disabled, based on the provided * boolean value. */	Import.prototype.setDisabled = function ( disabled ) { const that = this; this.disabled = disabled; return getWikitext( getFullTarget( this.target ) ).then( ( wikitext ) => {			const lineNums = that.getLineNums( wikitext ),				newWikitextLines = wikitext.split( '\n' );

if ( disabled ) { lineNums.forEach( ( lineNum ) => {					if ( newWikitextLines[ lineNum ].trim.indexOf( '//' ) !== 0 ) {						newWikitextLines[ lineNum ] = '//' + newWikitextLines[ lineNum ].trim;					}				} ); } else { lineNums.forEach( ( lineNum ) => {					if ( newWikitextLines[ lineNum ].trim.indexOf( '//' ) === 0 ) {						newWikitextLines[ lineNum ] = newWikitextLines[ lineNum ].replace( /^\s*\/\/\s*/, '' );					}				} ); }

const summary = ( disabled ? STRINGS.disableSummary : STRINGS.enableSummary ) .replace( '$1', that.getDescription( /* useWikitext */ true ) ) + ADVERT; return api.postWithEditToken( {				action: 'edit',				title: getFullTarget( that.target ),				summary: summary,				text: newWikitextLines.join( '\n' )			} ); } );	};

Import.prototype.toggleDisabled = function { this.disabled = !this.disabled; return this.setDisabled( this.disabled ); };

/**	 * Move this import to another file. */	Import.prototype.move = function ( newTarget ) { if ( this.target === newTarget ) { return; }		const old = new Import( this.page, this.wiki, this.url, this.target, this.disabled ); this.target = newTarget; return $.when( old.uninstall, this.install ); };

function getAllTargetWikitexts { return $.getJSON(			mw.util.wikiScript( 'api' ),			{				format: 'json',				action: 'query',				prop: 'revisions',				rvprop: 'content',				rvslots: 'main',				titles: SKINS.map( getFullTarget ).join( '|' )			}		).then( ( data ) => {			if ( data && data.query && data.query.pages ) {				const result = {};				Object.values( data.query.pages ).forEach( ( moreData ) => { const nameWithoutExtension = new mw.Title( moreData.title ).getNameText; const targetName = nameWithoutExtension.substring( nameWithoutExtension.indexOf( '/' ) + 1 ); result[ targetName ] = moreData.revisions ? moreData.revisions[ 0 ].slots.main[ '*' ] : null; } );				return result;			}		} ); }

function buildImportList { return getAllTargetWikitexts.then( ( wikitexts ) => {			Object.keys( wikitexts ).forEach( ( targetName ) => { const targetImports = []; if ( wikitexts[ targetName ] ) { const lines = wikitexts[ targetName ].split( '\n' ); let currImport; for ( let i = 0; i < lines.length; i++ ) { currImport = Import.fromJs( lines[ i ], targetName ); if ( currImport ) { targetImports.push( currImport ); scriptCount++; if ( currImport.type === 0 ) { if ( !localScriptsByName[ currImport.page ] ) { localScriptsByName[ currImport.page ] = []; }								localScriptsByName[ currImport.page ].push( currImport.target ); }						}					}				}				imports[ targetName ] = targetImports; } );		} );	}

/**	 * "Normalizes" (standardizes the format of) lines in the given * config page. */	function normalize( target ) { return getWikitext( getFullTarget( target ) ).then( ( wikitext ) => {			const lines = wikitext.split( '\n' ),				newLines = Array( lines.length );			let currImport;			for ( let i = 0; i < lines.length; i++ ) {				currImport = Import.fromJs( lines[ i ], target );				if ( currImport ) {					newLines[ i ] = currImport.toJs;				} else {					newLines[ i ] = lines[ i ];				}			}			return api.postWithEditToken( { action: 'edit', title: getFullTarget( target ), summary: STRINGS.normalizeSummary, text: newLines.join( '\n' ) } );		} );	}

function conditionalReload( openPanel ) { if ( window.scriptInstallerAutoReload ) { if ( openPanel ) { document.cookie = 'open_script_installer=yes'; }			window.location.reload( true ); }	}

/********************************************	 *	 * UI code *	 ********************************************/	function makePanel { const $list = $( ' ' ).attr( 'id', 'script-installer-panel' ) .append( $( ' ' ).text( STRINGS.panelHeader ) ); const $container = $( ' ' ).addClass( 'container' ).appendTo( $list );

// Container for checkboxes $container.append( $( ' ' )			.attr( 'class', 'checkbox-container' )			.append( $( ' ' )					.attr( { id: 'siNormalize', type: 'checkbox' } ) .on( 'click', => {						$( '.normalize-wrapper' ).toggle( 0 );					} ), $( ' ' )					.attr( 'for', 'siNormalize' ) .text( STRINGS.showNormalizeLinks ), $( ' ' )					.attr( { id: 'siMove', type: 'checkbox' } ) .on( 'click', => {						$( '.move-wrapper' ).toggle( 0 );					} ), $( ' ' )					.attr( 'for', 'siMove' ) .text( STRINGS.showMoveLinks ) ) ); if ( scriptCount > NUM_SCRIPTS_FOR_SEARCH ) { $container.append( $( ' ' )				.attr( 'class', 'filter-container' )				.append( $( ' ' )						.attr( 'for', 'siQuickFilter' ) .text( STRINGS.quickFilter ), $( ' ' )						.attr( { id: 'siQuickFilter', type: 'text' } ) .on( 'input', function {							const filterString = $( this ).val;							if ( filterString ) {								const sel = "#script-installer-panel li[name*='" +                                        $.escapeSelector( $( this ).val ) + "']";								$( '#script-installer-panel li.script' ).toggle( false );								$( sel ).toggle( true );							} else {								$( '#script-installer-panel li.script' ).toggle( true );							}						} ) ) );

// Now, get the checkboxes out of the way $container.find( '.checkbox-container' ) .css( 'float', 'right' ); }		$.each( imports, ( targetName, targetImports ) => {			const fmtTargetName = ( targetName === 'common' ? STRINGS.skinCommon : targetName );			if ( targetImports.length ) {				$container.append( $( ' ' ).append(						fmtTargetName,						$( ' ' )							.addClass( 'normalize-wrapper' )							.append( ' (',								$( '' )									.text( STRINGS.normalize )									.on( 'click', => { normalize( targetName ).done( => {											conditionalReload( true );										} ); } ),								')' )							.hide ), $( '' ).append(						targetImports.map( ( anImport ) => $( '' ) .addClass( 'script' ) .attr( 'name', anImport.getDescription ) .append(								$( '' )									.text( anImport.getDescription )									.addClass( 'script' )									.attr( 'href', anImport.getHumanUrl ),								' (', $( '' ) .text( STRINGS.uninstallLinkText ) .on( 'click', function {										$( this ).text( STRINGS.uninstallProgressMsg );										anImport.uninstall.done(  => { conditionalReload( true ); } );									} ),								' | ',								$( '' ) .text( anImport.disabled ? STRINGS.enableLinkText : STRINGS.disableLinkText ) .on( 'click', function {										$( this ).text( anImport.disabled ? STRINGS.enableProgressMsg : STRINGS.disableProgressMsg );										anImport.toggleDisabled.done( function { $( this ).toggleClass( 'disabled' ); conditionalReload( true ); } );									} ),								$( ' ' )									.addClass( 'move-wrapper' ) .append(										' | ',										$( '' )											.text( STRINGS.moveLinkText )											.on( 'click', function { let dest = null; const PROMPT = STRINGS.movePrompt + ' ' + SKINS.join( ', ' ); do { dest = ( window.prompt( PROMPT ) || '' ).toLowerCase; } while ( dest && SKINS.indexOf( dest ) < 0 ); if ( !dest ) { return; }												$( this ).text( STRINGS.moveProgressMsg ); anImport.move( dest ).done( => {													conditionalReload( true );												} ); } )									)									.hide, ')' )							.toggleClass( 'disabled', anImport.disabled ) ) ) );			}		} ); return $list; }

function buildCurrentPageInstallElement { let addingInstallLink = false; // will we be adding a legitimate install link? const $installElement = $( ' ' ); // only used if addingInstallLink is set to true

const namespaceNumber = mw.config.get( 'wgNamespaceNumber' ); const pageName = mw.config.get( 'wgPageName' );

// Namespace 2 is User if ( namespaceNumber === 2 &&               pageName.indexOf( '/' ) > 0 ) { const contentModel = mw.config.get( 'wgPageContentModel' ); if ( contentModel === 'javascript' ) { const prefixLength = mw.config.get( 'wgUserName' ).length + 6; if ( pageName.indexOf( USER_NAMESPACE_NAME + ':' + mw.config.get( 'wgUserName' ) ) === 0 ) { const skinIndex = SKINS.indexOf( pageName.substring( prefixLength ).slice( 0, -3 ) ); if ( skinIndex >= 0 ) { return $( ' ' ).text( STRINGS.cannotInstall ) .attr( 'title', STRINGS.cannotInstallSkin ); }				}				addingInstallLink = true; } else { return $( ' ' ).text( STRINGS.cannotInstall + ' (' + STRINGS.notJavaScript + ')' ) .attr( 'title', STRINGS.cannotInstallContentModel.replace( '$1', contentModel ) ); }		}

// Namespace 8 is MediaWiki if ( namespaceNumber === 8 ) { return $( '' ).text( STRINGS.installViaPreferences ) .attr( 'href', mw.util.getUrl( 'Special:Preferences' ) + '#mw-prefsection-gadgets' ); }

const editRestriction = mw.config.get( 'wgRestrictionEdit' ) || []; if ( ( namespaceNumber !== 2 && namespaceNumber !== 8 ) &&           ( editRestriction.indexOf( 'sysop' ) >= 0 || editRestriction.indexOf( 'editprotected' ) >= 0 ) ) { $installElement.append( ' ',				$( ' ' ).append( $( ' ' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Achtung-yellow.svg/20px-Achtung-yellow.svg.png' ).addClass( 'warning' ), STRINGS.insecure )					.attr( 'title', STRINGS.tempWarning ) ); addingInstallLink = true; }

if ( addingInstallLink ) { const fixedPageName = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' ); $installElement.prepend( $( '' )				.attr( 'id', 'script-installer-main-install' )				.text( localScriptsByName[ fixedPageName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )				.on( 'click', makeLocalInstallClickHandler( fixedPageName ) ) );

// If the script is installed but disabled, allow the user to enable it			const allScriptsInTarget = imports[ localScriptsByName[ fixedPageName ] ]; const importObj = allScriptsInTarget && allScriptsInTarget.find( ( anImport ) => anImport.page === fixedPageName ); if ( importObj && importObj.disabled ) { $installElement.append( ' | ',					$( '' )						.attr( 'id', 'script-installer-main-enable' )						.text( STRINGS.enableLinkText )						.on( 'click', function { $( this ).text( STRINGS.enableProgressMsg ); importObj.setDisabled( false ).done( => {								conditionalReload( false );							} ); } ) );			}			return $installElement; }

return $( ' ' ).text( STRINGS.cannotInstall + ' ' + STRINGS.insecure ) .attr( 'title', STRINGS.badPageError ); }

function showUi { $( '#firstHeading' ).append( $( ' ' )			.attr( 'id', 'script-installer-top-container' )			.append( buildCurrentPageInstallElement, ' | ',				$( '' ) .text( STRINGS.manageUserScripts ).on( 'click', => {						if ( !document.getElementById( 'script-installer-panel' ) ) {							$( '#mw-content-text' ).before( makePanel );						} else {							$( '#script-installer-panel' ).remove;						}					} ) ) ); }

function attachInstallLinks { // At the end of each transclusion, there is		//  $( 'span.scriptInstallerLink' ).each( function {			const scriptName = this.id;			$( this ).append( ' | ', $( '' ) .text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText ) .on( 'click', makeLocalInstallClickHandler( scriptName ) ) );		} );

$( 'table.infobox-user-script' ).each( function {			const $infoboxScriptField = $( this ).find( "th:contains('Source')" ).next;			let scriptName = mw.config.get( 'wgPageName' );			const isHyperlink = $infoboxScriptField.html !== $infoboxScriptField.text;			if ( isHyperlink ) {				const $link = $infoboxScriptField.find( 'a' ).first;				const isExternalLink = $link.hasClass( 'external' );				if ( !isExternalLink ) {					scriptName = /\/wiki\/(.*)/.exec( $link.attr( 'href' ) )[ 1 ];				}			} else {				scriptName = $infoboxScriptField.text;			}			scriptName = /user:.+?\/.+?.js/i.exec( scriptName )[ 0 ];			$( this ).children( 'tbody' ).append( $( ' ' ).append( $( ' ' )				.attr( 'colspan', '2' )				.addClass( 'script-installer-ibx' )				.append( $( ' ' ) .addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' ) .text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText ) .on( 'click', makeLocalInstallClickHandler( scriptName ) ) ) ) );		} ); }

function makeLocalInstallClickHandler( scriptName ) { return function { const $this = $( this ); if ( $this.text === STRINGS.installLinkText ) { const okay = window.confirm(					STRINGS.bigSecurityWarning.replace( '$1', STRINGS.securityWarningSection.replace( '$1', scriptName ) ) ); if ( okay ) { $( this ).text( STRINGS.installProgressMsg ); Import.ofLocal( scriptName, window.scriptInstallerInstallTarget ).install.done( => {						$( this ).text( STRINGS.uninstallLinkText );						conditionalReload( false );					} ); }			} else { $( this ).text( STRINGS.uninstallProgressMsg ); const uninstalls = uniques( localScriptsByName[ scriptName ] ) .map( ( target ) => Import.ofLocal( scriptName, target ).uninstall ); $.when.apply( $, uninstalls ).then( => {					$( this ).text( STRINGS.installLinkText );					conditionalReload( false );				} ); }		};	}

/********************************************	 *	 * Utility functions *	 ********************************************/

/**	 * Gets the wikitext of a page with the given title (namespace required). */	function getWikitext( title ) { return $.getJSON(			mw.util.wikiScript( 'api' ),			{				format: 'json',				action: 'query',				prop: 'revisions',				rvprop: 'content',				rvslots: 'main',				rvlimit: 1,				titles: title			}		).then( ( data ) => {			const pageId = Object.keys( data.query.pages )[ 0 ];			if ( data.query.pages[ pageId ].revisions ) {				return data.query.pages[ pageId ].revisions[ 0 ].slots.main[ '*' ];			}			return '';		} ); }

function escapeForRegex( s ) { return s.replace( /[-/\\^$*+?.|[\]{}]/g, '\\$&' ); }

/**	 * Escape a string for use in a JavaScript string literal. * This function is adapted from * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js * (released under the MIT licence). */	function escapeForJsString( s ) { return s.replace( /["'\\\n\r\u2028\u2029]/g, ( character ) => {			// Escape all characters not included in SingleStringCharacters and			// DoubleStringCharacters on			// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4			switch ( character ) {				case '"':				case "'":				case '\\':					return '\\' + character;					// Four possible LineTerminator characters need to be escaped:				case '\n':					return '\\n';				case '\r':					return '\\r';				case '\u2028':					return '\\u2028';				case '\u2029':					return '\\u2029';			}		} ); }

/**	 * Escape a string for use in an inline JavaScript comment (comments that	 * start with two slashes "//"). * This function is adapted from * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js * (released under the MIT licence). */	function escapeForJsComment( s ) { return s.replace( /[\n\r\u2028\u2029]/g, ( character ) => {			switch ( character ) {				// Escape possible LineTerminator characters				case '\n':					return '\\n';				case '\r':					return '\\r';				case '\u2028':					return '\\u2028';				case '\u2029':					return '\\u2029';			}		} ); }

/**	 * Unescape a JavaScript string literal. *	 * This is the inverse of escapeForJsString. */	function unescapeForJsString( s ) { return s.replace( /\\"|\\'|\\\\|\\n|\\r|\\u2028|\\u2029/g, ( substring ) => {			switch ( substring ) {				case '\\"':					return '"';				case "\\'":					return "'";				case '\\\\':					return '\\';				case '\\r':					return '\r';				case '\\n':					return '\n';				case '\\u2028':					return '\u2028';				case '\\u2029':					return '\u2029';			}		} );	}

function getFullTarget( target ) { return USER_NAMESPACE_NAME + ':' + mw.config.get( 'wgUserName' ) + '/' + target + '.js'; }

// From https://stackoverflow.com/a/10192255 function uniques( array ) { return array.filter( ( el, index, arr ) => index === arr.indexOf( el ) ); }

if ( window.scriptInstallerAutoReload === undefined ) { window.scriptInstallerAutoReload = true; }

if ( window.scriptInstallerInstallTarget === undefined ) { window.scriptInstallerInstallTarget = 'common'; // by default, install things to the user's common.js	}

const jsPage = mw.config.get( 'wgPageName' ).slice( -3 ) === '.js' || mw.config.get( 'wgPageContentModel' ) === 'javascript'; $.when(		$.ready,		mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] )	).then( => {		api = new mw.Api;		buildImportList.then(  => { attachInstallLinks; if ( jsPage ) { showUi; }

// Auto-open the panel if we set the cookie to do so (see `conditionalReload`) if ( document.cookie.indexOf( 'open_script_installer=yes' ) >= 0 ) { document.cookie = 'open_script_installer=; expires=Thu, 01 Jan 1970 00:00:01 GMT'; $( "#script-installer-top-container a:contains('Manage')" ).trigger( 'click' ); }		} );	} ); } );