'use strict'; let config = require( './config.json' ); const portletLinkOptions = require( './portletLinkOptions.json' ); const infinityValues = require( './infinityValues.json' ); require( './jquery.accessKeyLabel.js' ); /** * Encode the string like PHP's rawurlencode. * * @ignore * @param {string} str String to be encoded. * @return {string} Encoded string */ function rawurlencode( str ) { return encodeURIComponent( String( str ) ) .replace( /!/g, '%21' ) .replace( /'/g, '%27' ) .replace( /\(/g, '%28' ) .replace( /\)/g, '%29' ) .replace( /\*/g, '%2A' ) .replace( /~/g, '%7E' ); } /** * Private helper function used by util.escapeId*() * * @ignore * @param {string} str String to be encoded * @param {string} mode Encoding mode, see documentation at * MainConfigSchema::FragmentMode. * @return {string} Encoded string */ function escapeIdInternal( str, mode ) { str = String( str ); switch ( mode ) { case 'html5': return str.replace( / /g, '_' ); case 'legacy': return rawurlencode( str.replace( / /g, '_' ) ) .replace( /%3A/g, ':' ) .replace( /%/g, '.' ); default: throw new Error( 'Unrecognized ID escaping mode ' + mode ); } } /** * Library providing useful common skin-agnostic utility functions. Please see * [mediawiki.util]{@link module:mediawiki.util}. * * Alias for the [mediawiki.util]{@link module:mediawiki.util} module. * * @namespace mw.util */ /** * Utility library provided by the `mediawiki.util` ResourceLoader module. Accessible inside ResourceLoader modules * or for gadgets as part of the [mw global object]{@link mw}. * * @example * // Inside MediaWiki extensions * const util = require( 'mediawiki.util' ); * // In gadgets * const mwUtil = mw.util; * @exports mediawiki.util */ const util = { /** * Encode the string like PHP's rawurlencode. * * @method * @param {string} str String to be encoded. * @return {string} Encoded string */ rawurlencode: rawurlencode, /** * Encode a string as CSS id, for use as HTML id attribute value. * * Analog to `Sanitizer::escapeIdForAttribute()` in PHP. * * @since 1.30 * @param {string} str String to encode * @return {string} Encoded string */ escapeIdForAttribute: function ( str ) { return escapeIdInternal( str, config.FragmentMode[ 0 ] ); }, /** * Encode a string as URL fragment, for use as HTML anchor link. * * Analog to `Sanitizer::escapeIdForLink()` in PHP. * * @since 1.30 * @param {string} str String to encode * @return {string} Encoded string */ escapeIdForLink: function ( str ) { return escapeIdInternal( str, config.FragmentMode[ 0 ] ); }, /** * Get the target element from a link hash. * * This is the same element as you would get from * document.querySelectorAll(':target'), but can be used on * an arbitrary hash fragment, or after pushState/replaceState * has been used. * * Link fragments can be unencoded, fully encoded or partially * encoded, as defined in the spec. * * We can't just use decodeURI as that assumes the fragment * is fully encoded, and throws an error on a string like '%A', * so we use the percent-decode. * * @param {string} [hash] Hash fragment, without the leading '#'. * Taken from location.hash if omitted. * @return {HTMLElement|null} Element, if found */ getTargetFromFragment: function ( hash ) { hash = hash || location.hash.slice( 1 ); if ( !hash ) { // Firefox emits a console warning if you pass an empty string // to getElementById (T272844). return null; } // Per https://html.spec.whatwg.org/multipage/browsing-the-web.html#target-element // we try the raw fragment first, then the percent-decoded fragment. const element = document.getElementById( hash ); if ( element ) { return element; } const decodedHash = this.percentDecodeFragment( hash ); if ( !decodedHash ) { // decodedHash can return null, calling getElementById would cast it to a string return null; } return document.getElementById( decodedHash ); }, /** * Percent-decode a string, as found in a URL hash fragment. * * Implements the percent-decode method as defined in * https://url.spec.whatwg.org/#percent-decode. * * URLSearchParams implements https://url.spec.whatwg.org/#concept-urlencoded-parser * which performs a '+' to ' ' substitution before running percent-decode. * * To get the desired behaviour we percent-encode any '+' in the fragment * to effectively expose the percent-decode implementation. * * @param {string} text Text to decode * @return {string|null} Decoded text, null if decoding failed */ percentDecodeFragment: function ( text ) { const params = new URLSearchParams( 'q=' + text // Query string param decoding replaces '+' with ' ' before doing the // percent_decode, so encode '+' to prevent this. .replace( /\+/g, '%2B' ) // Query strings are split on '&' and then '=' so encode these too. .replace( /&/g, '%26' ) .replace( /=/g, '%3D' ) ); return params.get( 'q' ); }, /** * Return a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. * * Ported from Underscore.js 1.5.2, Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud * and Investigative Reporters & Editors, distributed under the MIT license, from * . * * @since 1.34 * @param {Function} func Function to debounce * @param {number} [wait=0] Wait period in milliseconds * @param {boolean} [immediate] Trigger on leading edge * @return {Function} Debounced function */ debounce: function ( func, wait, immediate ) { // Old signature (wait, func). if ( typeof func === 'number' ) { const tmpWait = wait; wait = func; func = tmpWait; } let timeout; return function () { const context = this, args = arguments, later = function () { timeout = null; if ( !immediate ) { func.apply( context, args ); } }; if ( immediate && !timeout ) { func.apply( context, args ); } if ( !timeout || wait ) { clearTimeout( timeout ); timeout = setTimeout( later, wait ); } }; }, /** * Return a function, that, when invoked, will only be triggered at most once * during a given window of time. If called again during that window, it will * wait until the window ends and then trigger itself again. * * As it's not knowable to the caller whether the function will actually run * when the wrapper is called, return values from the function are entirely * discarded. * * Ported from OOUI. * * @param {Function} func Function to throttle * @param {number} wait Throttle window length, in milliseconds * @return {Function} Throttled function */ throttle: function ( func, wait ) { let context, args, timeout, previous = Date.now() - wait; const run = function () { timeout = null; previous = Date.now(); func.apply( context, args ); }; return function () { // Check how long it's been since the last time the function was // called, and whether it's more or less than the requested throttle // period. If it's less, run the function immediately. If it's more, // set a timeout for the remaining time -- but don't replace an // existing timeout, since that'd indefinitely prolong the wait. const remaining = Math.max( wait - ( Date.now() - previous ), 0 ); context = this; args = arguments; if ( !timeout ) { // If time is up, do setTimeout( run, 0 ) so the function // always runs asynchronously, just like Promise#then . timeout = setTimeout( run, remaining ); } }; }, /** * Encode page titles in a way that matches `wfUrlencode` in PHP. * * This is important both for readability and consistency in the user experience, * as well as for caching. If URLs are not formatted in the canonical way, they * may be subject to drastically shorter cache durations and/or miss automatic * purging after edits, thus leading to stale content being served from a * non-canonical URL. * * @method * @param {string} str String to be encoded. * @return {string} Encoded string */ wikiUrlencode: mw.internalWikiUrlencode, /** * Get the URL to a given local wiki page name. * * @param {string|null} [pageName=wgPageName] Page name * @param {Object} [params] A mapping of query parameter names to values, * e.g. `{ action: 'edit' }` * @return {string} URL, relative to `wgServer`. */ getUrl: function ( pageName, params ) { let url, query, fragment, title = typeof pageName === 'string' ? pageName : mw.config.get( 'wgPageName' ); // Find any fragment const fragmentIdx = title.indexOf( '#' ); if ( fragmentIdx !== -1 ) { fragment = title.slice( fragmentIdx + 1 ); // Exclude the fragment from the page name title = title.slice( 0, fragmentIdx ); } // Produce query string if ( params ) { query = $.param( params ); } if ( !title && fragment ) { // If only a fragment was given, make a fragment-only link (T288415) url = ''; } else if ( query ) { url = title ? util.wikiScript() + '?title=' + util.wikiUrlencode( title ) + '&' + query : util.wikiScript() + '?' + query; } else { // Specify a function as the replacement, // so that "$" characters in title are not interpreted. url = mw.config.get( 'wgArticlePath' ) .replace( '$1', () => util.wikiUrlencode( title ) ); } // Append the encoded fragment if ( fragment ) { url += '#' + util.escapeIdForLink( fragment ); } return url; }, /** * Get URL to a MediaWiki entry point. * * Similar to `wfScript()` in PHP. * * @since 1.18 * @param {string} [str="index"] Name of entry point (e.g. 'index' or 'api') * @return {string} URL to the script file (e.g. `/w/api.php`) */ wikiScript: function ( str ) { if ( !str || str === 'index' ) { return mw.config.get( 'wgScript' ); } else if ( str === 'load' ) { return config.LoadScript; } else { return mw.config.get( 'wgScriptPath' ) + '/' + str + '.php'; } }, /** * Append a new style block to the head and return the CSSStyleSheet object. * * To access the `