Jump to content

MediaWiki talk:Gadget-script-installer-core.js/sandbox

Page contents not supported in other languages.
From Wikipedia, the free encyclopedia

( 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( $( '<header>' ).text( STRINGS.panelHeader ) );

const $container = $( '
' ).addClass( 'container' ).appendTo( $list );

// Container for checkboxes

$container.append( $( '
' )

.attr( 'class', 'checkbox-container' ) .append( $( '<input>' ) .attr( { id: 'siNormalize', type: 'checkbox' } ) .on( 'click', () => { $( '.normalize-wrapper' ).toggle( 0 ); } ), $( '<label>' ) .attr( 'for', 'siNormalize' ) .text( STRINGS.showNormalizeLinks ), $( '<input>' ) .attr( { id: 'siMove', type: 'checkbox' } ) .on( 'click', () => { $( '.move-wrapper' ).toggle( 0 ); } ), $( '<label>' ) .attr( 'for', 'siMove' ) .text( STRINGS.showMoveLinks ) ) ); if ( scriptCount > NUM_SCRIPTS_FOR_SEARCH ) {

$container.append( $( '
' )

.attr( 'class', 'filter-container' ) .append( $( '<label>' ) .attr( 'for', 'siQuickFilter' ) .text( STRINGS.quickFilter ), $( '<input>' ) .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( ' (', $( '<a>' ) .text( STRINGS.normalize ) .on( 'click', () => { normalize( targetName ).done( () => { conditionalReload( true ); } ); } ), ')' ) .hide() ), $( '
    ' ).append( targetImports.map( ( anImport ) => $( '
  • ' ) .addClass( 'script' ) .attr( 'name', anImport.getDescription() ) .append( $( '<a>' ) .text( anImport.getDescription() ) .addClass( 'script' ) .attr( 'href', anImport.getHumanUrl() ), ' (', $( '<a>' ) .text( STRINGS.uninstallLinkText ) .on( 'click', function () { $( this ).text( STRINGS.uninstallProgressMsg ); anImport.uninstall().done( () => { conditionalReload( true ); } ); } ), ' | ', $( '<a>' ) .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( ' | ', $( '<a>' ) .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 $( '<a>' ).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( $( '<img>' ).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( $( '<a>' ) .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( ' | ', $( '<a>' ) .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(), ' | ', $( '<a>' ) .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 [1] (source) transclusion, there is // $( 'span.scriptInstallerLink' ).each( function () { const scriptName = this.id.replace( /_/g, ' ' ); // if the script name happens to contain underscores, this purges it $( this ).append( ' | ', $( '<a>' ) .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 = scriptName.replace( /_/g, ' ' ); // if the script name happens to contain underscores, this purges it scriptName = /user:.+?\/.+?.js/i.exec( scriptName )[ 0 ]; $( this ).children( 'tbody' ).append( $( '' ).append( $( '' )

    .attr( 'colspan', '2' ) .addClass( 'script-installer-ibx' ) .append( $( '<button>' ) .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' ); } } ); } ); }() );

    1. ^ Copy the following code, edit your user JavaScript, then paste:
      {{subst:lusc|1= .js}}