Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.base / mediawiki.base.js
bloba104d1f7621797b10c58b93507d4c6e2c5b04b5d
1 'use strict';
3 const slice = Array.prototype.slice;
5 // Apply site-level data
6 mw.config.set( require( './config.json' ) );
8 require( './log.js' );
10 /**
11  * @class mw.Message
12  * @classdesc Describes a translateable text or HTML string. Similar to the Message class in MediaWiki PHP.
13  *
14  * @example
15  * var obj, str;
16  * mw.messages.set( {
17  *     'hello': 'Hello world',
18  *     'hello-user': 'Hello, $1!',
19  *     'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3',
20  *     'so-unusual': 'You will find: $1'
21  * } );
22  *
23  * obj = mw.message( 'hello' );
24  * mw.log( obj.text() );
25  * // Hello world
26  *
27  * obj = mw.message( 'hello-user', 'John Doe' );
28  * mw.log( obj.text() );
29  * // Hello, John Doe!
30  *
31  * obj = mw.message( 'welcome-user', 'John Doe', 'Wikipedia', '2 hours ago' );
32  * mw.log( obj.text() );
33  * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
34  *
35  * // Using mw.msg shortcut, always in "text' format.
36  * str = mw.msg( 'hello-user', 'John Doe' );
37  * mw.log( str );
38  * // Hello, John Doe!
39  *
40  * // Different formats
41  * obj = mw.message( 'so-unusual', 'Time "after" <time>' );
42  *
43  * mw.log( obj.text() );
44  * // You will find: Time "after" <time>
45  *
46  * mw.log( obj.escaped() );
47  * // You will find: Time &quot;after&quot; &lt;time&gt;
48  *
49  * @constructor
50  * @description Object constructor for messages. The constructor is not publicly accessible;
51  * use {@link mw.message} instead.
52  * @param {mw.Map} map Message store
53  * @param {string} key
54  * @param {Array} [parameters]
55  */
56 function Message( map, key, parameters ) {
57         this.map = map;
58         this.key = key;
59         this.parameters = parameters || [];
62 Message.prototype = /** @lends mw.Message.prototype */ {
63         /**
64          * Get parsed contents of the message.
65          *
66          * The default parser does simple $N replacements and nothing else.
67          * This may be overridden to provide a more complex message parser.
68          * The primary override is in the mediawiki.jqueryMsg module.
69          *
70          * This function will not be called for nonexistent messages.
71          * For internal use by mediawiki.jqueryMsg only
72          *
73          * @private
74          * @param {string} format
75          * @return {string} Parsed message
76          */
77         parser: function ( format ) {
78                 let text = this.map.get( this.key );
80                 // Apply qqx formatting.
81                 //
82                 // - Keep this synchronised with LanguageQqx/MessageCache in PHP.
83                 // - Keep this synchronised with mw.jqueryMsg.Parser#getAst.
84                 //
85                 // Unlike LanguageQqx in PHP, this doesn't replace unconditionally.
86                 // It replaces non-existent messages, and messages that were exported by
87                 // load.php as "(key)" in qqx formatting. Some extensions export other data
88                 // via their message blob (T222944).
89                 if (
90                         mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
91                         ( !text || text === '(' + this.key + ')' )
92                 ) {
93                         text = '(' + this.key + '$*)';
94                 }
95                 text = mw.format( text, ...this.parameters );
96                 if ( format === 'parse' ) {
97                         // We don't know how to parse anything, so escape it all
98                         text = mw.html.escape( text );
99                 }
100                 return text;
101         },
103         /**
104          * Add (does not replace) parameters for `$N` placeholder values.
105          *
106          * @param {Array} parameters
107          * @return {mw.Message}
108          * @chainable
109          */
110         params: function ( parameters ) {
111                 this.parameters.push( ...parameters );
112                 return this;
113         },
115         /**
116          * Convert message object to a string using the "text"-format .
117          *
118          * This exists for implicit string type casting only.
119          * Do not call this directly. Use mw.Message#text() instead, one of the
120          * other format methods.
121          *
122          * @private
123          * @param {string} [format="text"] Internal parameter. Uses "text" if called
124          *  implicitly through string casting.
125          * @return {string} Message in the given format, or `⧼key⧽` if the key
126          *  does not exist.
127          */
128         toString: function ( format ) {
129                 if ( !this.exists() ) {
130                         // Make sure qqx works for non-existent messages, see parser() above.
131                         if ( mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) {
132                                 // Use ⧼key⧽ as text if key does not exist
133                                 // Err on the side of safety, ensure that the output
134                                 // is always html safe in the event the message key is
135                                 // missing, since in that case its highly likely the
136                                 // message key is user-controlled.
137                                 // '⧼' is used instead of '<' to side-step any
138                                 // double-escaping issues.
139                                 // (Keep synchronised with Message::toString() in PHP.)
140                                 return '⧼' + mw.html.escape( this.key ) + '⧽';
141                         }
142                 }
144                 if ( !format ) {
145                         format = 'text';
146                 }
148                 if ( format === 'plain' || format === 'text' || format === 'parse' ) {
149                         return this.parser( format );
150                 }
152                 // Format: 'escaped' (including for any invalid format, default to safe escape)
153                 return mw.html.escape( this.parser( 'escaped' ) );
154         },
156         /**
157          * Parse message as wikitext and return HTML.
158          *
159          * If jqueryMsg is loaded, this transforms text and parses a subset of supported wikitext
160          * into HTML. Without jqueryMsg, it is equivalent to {@link mw.Message#escaped}.
161          *
162          * @return {string} String form of parsed message
163          */
164         parse: function () {
165                 return this.toString( 'parse' );
166         },
168         /**
169          * Return message plainly.
170          *
171          * This substitutes parameters, but otherwise does not transform the
172          * message content.
173          *
174          * @return {string} String form of plain message
175          */
176         plain: function () {
177                 return this.toString( 'plain' );
178         },
180         /**
181          * Format message with text transformations applied.
182          *
183          * If jqueryMsg is loaded, `{{`-transformation is done for supported
184          * magic words such as `{{plural:}}`, `{{gender:}}`, and `{{int:}}`.
185          * Without jqueryMsg, it is equivalent to {@link mw.Message#plain}.
186          *
187          * @return {string} String form of text message
188          */
189         text: function () {
190                 return this.toString( 'text' );
191         },
193         /**
194          * Format message and return as escaped text in HTML.
195          *
196          * This is equivalent to the #text format, which is then HTML-escaped.
197          *
198          * @return {string} String form of html escaped message
199          */
200         escaped: function () {
201                 return this.toString( 'escaped' );
202         },
204         /**
205          * Check if a message exists. Equivalent to {@link mw.Map.exists}.
206          *
207          * @return {boolean}
208          */
209         exists: function () {
210                 return this.map.exists( this.key );
211         }
215  * @class mw
216  * @singleton
217  * @borrows mediawiki.inspect.runReports as inspect
218  */
221  * Empty object for third-party libraries, for cases where you don't
222  * want to add a new global, or the global is bad and needs containment
223  * or wrapping.
225  * @type {Object}
226  */
227 mw.libs = {};
230  * OOUI widgets specific to MediaWiki.
231  * Initially empty. To expand the amount of available widgets the `mediawiki.widget` module can be loaded.
233  * @namespace mw.widgets
234  * @example
235  * mw.loader.using('mediawiki.widget').then(() => {
236  *   OO.ui.getWindowManager().addWindows( [ new mw.widget.AbandonEditDialog() ] );
237  * });
238  */
239 mw.widgets = {};
242  * Generates a ResourceLoader report using the
243  * {@link mediawiki.inspect.js.html|mediawiki.inspect module}.
245  * @ignore
246  */
247 mw.inspect = function ( ...reports ) {
248         // Lazy-load
249         mw.loader.using( 'mediawiki.inspect', () => {
250                 mw.inspect.runReports( ...reports );
251         } );
255  * Replace `$*` with a list of parameters for `uselang=qqx` support.
257  * @private
258  * @since 1.33
259  * @param {string} formatString Format string
260  * @param {Array} parameters Values for $N replacements
261  * @return {string} Transformed format string
262  */
263 mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
264         if ( formatString.indexOf( '$*' ) !== -1 ) {
265                 let replacement = '';
266                 if ( parameters.length ) {
267                         replacement = ': ' + parameters.map( ( _, i ) => '$' + ( i + 1 ) ).join( ', ' );
268                 }
269                 return formatString.replace( '$*', replacement );
270         }
271         return formatString;
275  * Encode page titles in a way that matches `wfUrlencode` in PHP.
277  * @see mw.util#wikiUrlencode
278  * @private
279  * @param {string} str
280  * @return {string}
281  */
282 mw.internalWikiUrlencode = function ( str ) {
283         return encodeURIComponent( String( str ) )
284                 .replace( /'/g, '%27' )
285                 .replace( /%20/g, '_' )
286                 .replace( /%3B/g, ';' )
287                 .replace( /%40/g, '@' )
288                 .replace( /%24/g, '$' )
289                 .replace( /%2C/g, ',' )
290                 .replace( /%2F/g, '/' )
291                 .replace( /%3A/g, ':' );
295  * Format a string. Replace $1, $2 ... $N with positional arguments.
297  * Used by {@link mw.Message#parse}.
299  * @memberof mw
300  * @since 1.25
301  * @param {string} formatString Format string
302  * @param {...Mixed} parameters Values for $N replacements
303  * @return {string} Formatted string
304  */
305 mw.format = function ( formatString, ...parameters ) {
306         formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
307         return formatString.replace( /\$(\d+)/g, ( str, match ) => {
308                 const index = parseInt( match, 10 ) - 1;
309                 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
310         } );
313 // Expose Message constructor
314 mw.Message = Message;
317  * Get a message object.
319  * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
321  * @memberof mw
322  * @see {@link mw.Message}
323  * @param {string} key Key of message to get
324  * @param {...Mixed} parameters Values for $N replacements
325  * @return {mw.Message}
326  */
327 mw.message = function ( key ) {
328         const parameters = slice.call( arguments, 1 );
329         return new Message( mw.messages, key, parameters );
333  * Get a message string using the (default) 'text' format.
335  * Shortcut for `mw.message( key, parameters... ).text()`.
337  * @memberof mw
338  * @see {@link mw.Message}
339  * @param {string} key Key of message to get
340  * @param {...any} parameters Values for $N replacements
341  * @return {string}
342  */
343 mw.msg = function ( key, ...parameters ) {
344         // Shortcut must process text transformations by default
345         // if mediawiki.jqueryMsg is loaded. (T46459)
346         // eslint-disable-next-line mediawiki/msg-doc
347         return mw.message( key, ...parameters ).text();
351  * Convenience method for loading and accessing the
352  * {@link mw.notification.notify|mw.notification module}.
354  * @memberof mw
355  * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
356  * @param {Object} [options] See mw.notification#defaults for the defaults.
357  * @return {jQuery.Promise}
358  */
359 mw.notify = function ( message, options ) {
360         // Lazy load
361         return mw.loader.using( 'mediawiki.notification' ).then( () => mw.notification.notify( message, options ) );
364 const trackCallbacks = $.Callbacks( 'memory' );
365 let trackHandlers = [];
368  * Track an analytic event.
370  * This method provides a generic means for MediaWiki JavaScript code to capture state
371  * information for analysis. Each logged event specifies a string topic name that describes
372  * the kind of event that it is. Topic names consist of dot-separated path components,
373  * arranged from most general to most specific. Each path component should have a clear and
374  * well-defined purpose.
376  * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
377  * events that match their subscription, including buffered events that fired before the handler
378  * was subscribed.
380  * @memberof mw
381  * @param {string} topic Topic name
382  * @param {...Object|number|string} [data] Data describing the event.
383  */
384 mw.track = function ( topic, ...data ) {
385         mw.trackQueue.push( { topic, args: data } );
386         trackCallbacks.fire( mw.trackQueue );
390  * Register a handler for subset of analytic events, specified by topic.
392  * Handlers will be called once for each tracked event, including for any buffered events that
393  * fired before the handler was subscribed. The callback is passed a `topic` string, and optional
394  * `data` argument(s).
396  * @example
397  * // To monitor all topics for debugging
398  * mw.trackSubscribe( '', console.log );
400  * @example
401  * // To subscribe to any of `foo.*`, e.g. both `foo.bar` and `foo.quux`
402  * mw.trackSubscribe( 'foo.', console.log );
404  * @memberof mw
405  * @param {string} topic Handle events whose name starts with this string prefix
406  * @param {Function} callback Handler to call for each matching tracked event
407  * @param {string} callback.topic
408  * @param {...Object|number|string} [callback.data]
409  */
410 mw.trackSubscribe = function ( topic, callback ) {
411         let seen = 0;
412         function handler( trackQueue ) {
413                 for ( ; seen < trackQueue.length; seen++ ) {
414                         const event = trackQueue[ seen ];
415                         if ( event.topic.indexOf( topic ) === 0 ) {
416                                 callback( event.topic, ...event.args );
417                         }
418                 }
419         }
421         trackHandlers.push( [ handler, callback ] );
422         trackCallbacks.add( handler );
426  * Stop handling events for a particular handler.
428  * @memberof mw
429  * @param {Function} callback
430  */
431 mw.trackUnsubscribe = function ( callback ) {
432         trackHandlers = trackHandlers.filter( ( fns ) => {
433                 if ( fns[ 1 ] === callback ) {
434                         trackCallbacks.remove( fns[ 0 ] );
435                         // Ensure the tuple is removed to avoid holding on to closures
436                         return false;
437                 }
438                 return true;
439         } );
442 // Notify subscribers of any mw.trackQueue.push() calls
443 // from the startup module before mw.track() is defined.
444 trackCallbacks.fire( mw.trackQueue );
447  * @namespace Hooks
448  * @description Registry and firing of events.
450  * MediaWiki has various interface components that are extended, enhanced
451  * or manipulated in some other way by extensions, gadgets and even
452  * in core itself.
454  * This framework helps streamlining the timing of when these other
455  * code paths fire their plugins (instead of using document-ready,
456  * which can and should be limited to firing only once).
458  * Features like navigating to other wiki pages, previewing an edit
459  * and editing itself – without a refresh – can then retrigger these
460  * hooks accordingly to ensure everything still works as expected.
461  * See {@link Hook}.
463  * Example usage:
464  * ```
465  * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
466  * mw.hook( 'wikipage.content' ).fire( $content );
467  * ```
469  * Handlers can be added and fired for arbitrary event names at any time. The same
470  * event can be fired multiple times. The last run of an event is memorized
471  * (similar to `$(document).ready` and `$.Deferred().done`).
472  * This means if an event is fired, and a handler added afterwards, the added
473  * function will be fired right away with the last given event data.
475  * Like Deferreds and Promises, the {@link mw.hook} object is both detachable and chainable.
476  * Thus allowing flexible use and optimal maintainability and authority control.
477  * You can pass around the `add` and/or `fire` method to another piece of code
478  * without it having to know the event name (or {@link mw.hook} for that matter).
480  * ```
481  * var h = mw.hook( 'bar.ready' );
482  * new mw.Foo( .. ).fetch( { callback: h.fire } );
483  * ```
485  * The function signature for hooks can be considered [stable](https://www.mediawiki.org/wiki/Special:MyLanguage/Stable_interface_policy/Frontend).
486  * See available global events below.
487  */
489 const hooks = Object.create( null );
492  * Create an instance of {@link Hook}.
494  * @example
495  * const hook = mw.hook( 'name' );
496  * hook.add( () => alert( 'Hook was fired' ) );
497  * hook.fire();
499  * @param {string} name Name of hook.
500  * @return {Hook}
501  */
502 mw.hook = function ( name ) {
503         return hooks[ name ] || ( hooks[ name ] = ( function () {
504                 let memory;
505                 const fns = [];
506                 function rethrow( e ) {
507                         setTimeout( () => {
508                                 throw e;
509                         } );
510                 }
511                 /**
512                  * @class Hook
513                  * @classdesc An instance of a hook, created via [mw.hook method]{@link mw.hook}.
514                  * @global
515                  * @hideconstructor
516                  */
517                 return {
518                         /**
519                          * Register a hook handler.
520                          *
521                          * @param {...Function} handler Function to bind.
522                          * @memberof Hook
523                          * @return {Hook}
524                          */
525                         add: function () {
526                                 for ( let i = 0; i < arguments.length; i++ ) {
527                                         fns.push( arguments[ i ] );
528                                         if ( memory ) {
529                                                 try {
530                                                         arguments[ i ].apply( null, memory );
531                                                 } catch ( e ) {
532                                                         rethrow( e );
533                                                 }
534                                         }
535                                 }
536                                 return this;
537                         },
538                         /**
539                          * Unregister a hook handler.
540                          *
541                          * @param {...Function} handler Function to unbind.
542                          * @memberof Hook
543                          * @return {Hook}
544                          */
545                         remove: function () {
546                                 for ( let i = 0; i < arguments.length; i++ ) {
547                                         let j;
548                                         while ( ( j = fns.indexOf( arguments[ i ] ) ) !== -1 ) {
549                                                 fns.splice( j, 1 );
550                                         }
551                                 }
552                                 return this;
553                         },
554                         /**
555                          * Call hook handlers with data.
556                          *
557                          * @memberof Hook
558                          * @param {...any} data
559                          * @return {Hook}
560                          * @chainable
561                          */
562                         fire: function () {
563                                 for ( let i = 0; i < fns.length; i++ ) {
564                                         try {
565                                                 fns[ i ].apply( null, arguments );
566                                         } catch ( e ) {
567                                                 rethrow( e );
568                                         }
569                                 }
570                                 memory = slice.call( arguments );
571                                 return this;
572                         }
573                 };
574         }() ) );
578  * HTML construction helper functions.
580  * @example
581  * var Html, output;
583  * Html = mw.html;
584  * output = Html.element( 'div', {}, new Html.Raw(
585  *     Html.element( 'img', { src: '<' } )
586  * ) );
587  * mw.log( output ); // <div><img src="&lt;"/></div>
589  * @namespace mw.html
590  */
592 function escapeCallback( s ) {
593         switch ( s ) {
594                 case '\'':
595                         return '&#039;';
596                 case '"':
597                         return '&quot;';
598                 case '<':
599                         return '&lt;';
600                 case '>':
601                         return '&gt;';
602                 case '&':
603                         return '&amp;';
604         }
606 mw.html = {
607         /**
608          * Escape a string for HTML.
609          *
610          * Converts special characters to HTML entities.
611          *
612          * @example
613          * mw.html.escape( '< > \' & "' );
614          * // Returns &lt; &gt; &#039; &amp; &quot;
615          *
616          * @param {string} s The string to escape
617          * @return {string} HTML
618          */
619         escape: function ( s ) {
620                 return s.replace( /['"<>&]/g, escapeCallback );
621         },
623         /**
624          * Create an HTML element string, with safe escaping.
625          *
626          * @param {string} name The tag name.
627          * @param {Object} [attrs] An object with members mapping element names to values
628          * @param {string|mw.html.Raw|null} [contents=null] The contents of the element.
629          *
630          *  - string: Text to be escaped.
631          *  - null: The element is treated as void with short closing form, e.g. `<br/>`.
632          *  - this.Raw: The raw value is directly included.
633          * @return {string} HTML
634          */
635         element: function ( name, attrs, contents ) {
636                 let s = '<' + name;
638                 if ( attrs ) {
639                         for ( const attrName in attrs ) {
640                                 let v = attrs[ attrName ];
641                                 // Convert name=true, to name=name
642                                 if ( v === true ) {
643                                         v = attrName;
644                                         // Skip name=false
645                                 } else if ( v === false ) {
646                                         continue;
647                                 }
648                                 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
649                         }
650                 }
651                 if ( contents === undefined || contents === null ) {
652                         // Self close tag
653                         s += '/>';
654                         return s;
655                 }
656                 // Regular open tag
657                 s += '>';
658                 if ( typeof contents === 'string' ) {
659                         // Escaped
660                         s += this.escape( contents );
661                 } else if ( typeof contents === 'number' || typeof contents === 'boolean' ) {
662                         // Convert to string
663                         s += String( contents );
664                 } else if ( contents instanceof this.Raw ) {
665                         // Raw HTML inclusion
666                         s += contents.value;
667                 } else {
668                         throw new Error( 'Invalid content type' );
669                 }
670                 s += '</' + name + '>';
671                 return s;
672         },
674         /**
675          * @classdesc Wrapper object for raw HTML. Can be used with {@link mw.html.element}.
676          * @class mw.html.Raw
677          * @param {string} value
678          * @example
679          * const raw = new mw.html.Raw( 'Text' );
680          * mw.html.element( 'div', { class: 'html' }, raw );
681          */
682         Raw: function ( value ) {
683                 this.value = value;
684         }
688  * Schedule a function to run once the page is ready (DOM loaded).
690  * @since 1.5.8
691  * @memberof window
692  * @param {Function} fn
693  */
694 window.addOnloadHook = function ( fn ) {
695         $( () => {
696                 fn();
697         } );
700 const loadedScripts = {};
703  * Import a script using an absolute URI.
705  * @since 1.12.2
706  * @memberof window
707  * @param {string} url
708  * @return {HTMLElement|null} Script tag, or null if it was already imported before
709  */
710 window.importScriptURI = function ( url ) {
711         if ( loadedScripts[ url ] ) {
712                 return null;
713         }
714         loadedScripts[ url ] = true;
715         return mw.loader.addScriptTag( url );
719  * Import a local JS content page, for use by user scripts and site-wide scripts.
721  * Note that if the same title is imported multiple times, it will only
722  * be loaded and executed once.
724  * @since 1.12.2
725  * @memberof window
726  * @param {string} title
727  * @return {HTMLElement|null} Script tag, or null if it was already imported before
728  */
729 window.importScript = function ( title ) {
730         return window.importScriptURI(
731                 mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
732                         '&action=raw&ctype=text/javascript'
733         );
737  * Import a local CSS content page, for use by user scripts and site-wide scripts.
739  * @since 1.12.2
740  * @memberof window
741  * @param {string} title
742  * @return {HTMLElement} Link tag
743  */
744 window.importStylesheet = function ( title ) {
745         return mw.loader.addLinkTag(
746                 mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
747                         '&action=raw&ctype=text/css'
748         );
752  * Import a stylesheet using an absolute URI.
754  * @since 1.12.2
755  * @memberof window
756  * @param {string} url
757  * @param {string} media
758  * @return {HTMLElement} Link tag
759  */
760 window.importStylesheetURI = function ( url, media ) {
761         return mw.loader.addLinkTag( url, media );
765  * Get the names of all registered ResourceLoader modules.
767  * @memberof mw.loader
768  * @return {string[]}
769  */
770 mw.loader.getModuleNames = function () {
771         return Object.keys( mw.loader.moduleRegistry );
775  * Execute a function after one or more modules are ready.
777  * Use this method if you need to dynamically control which modules are loaded
778  * and/or when they loaded (instead of declaring them as dependencies directly
779  * on your module.)
781  * This uses the same loader as for regular module dependencies. This means
782  * ResourceLoader will not re-download or re-execute a module for the second
783  * time if something else already needed it. And the same browser HTTP cache,
784  * and localStorage are checked before considering to fetch from the network.
785  * And any on-going requests from other dependencies or using() calls are also
786  * automatically re-used.
788  * Example of inline dependency on OOjs:
789  * ```
790  * mw.loader.using( 'oojs', function () {
791  *     OO.compare( [ 1 ], [ 1 ] );
792  * } );
793  * ```
795  * Example of inline dependency obtained via `require()`:
796  * ```
797  * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
798  *     var util = require( 'mediawiki.util' );
799  * } );
800  * ```
802  * Since MediaWiki 1.23 this returns a promise.
804  * Since MediaWiki 1.28 the promise is resolved with a `require` function.
806  * @memberof mw.loader
807  * @param {string|Array} dependencies Module name or array of modules names the
808  *  callback depends on to be ready before executing
809  * @param {Function} [ready] Callback to execute when all dependencies are ready
810  * @param {Function} [error] Callback to execute if one or more dependencies failed
811  * @return {jQuery.Promise} With a `require` function
812  */
813 mw.loader.using = function ( dependencies, ready, error ) {
814         const deferred = $.Deferred();
816         // Allow calling with a single dependency as a string
817         if ( !Array.isArray( dependencies ) ) {
818                 dependencies = [ dependencies ];
819         }
821         if ( ready ) {
822                 deferred.done( ready );
823         }
824         if ( error ) {
825                 deferred.fail( error );
826         }
828         try {
829                 // Resolve entire dependency map
830                 dependencies = mw.loader.resolve( dependencies );
831         } catch ( e ) {
832                 return deferred.reject( e ).promise();
833         }
835         mw.loader.enqueue(
836                 dependencies,
837                 () => {
838                         deferred.resolve( mw.loader.require );
839                 },
840                 deferred.reject
841         );
843         return deferred.promise();
847  * Load a script by URL.
849  * @example
850  * mw.loader.getScript(
851  *     'https://example.org/x-1.0.0.js'
852  * )
853  *     .then( function () {
854  *         // Script succeeded. You can use X now.
855  *     }, function ( e ) {
856  *         // Script failed. X is not avaiable
857  *         mw.log.error( e.message ); // => "Failed to load script"
858  *     } );
859  * } );
861  * @memberof mw.loader
862  * @param {string} url Script URL
863  * @return {jQuery.Promise} Resolved when the script is loaded
864  */
865 mw.loader.getScript = function ( url ) {
866         return $.ajax( url, { dataType: 'script', cache: true } )
867                 .catch( () => {
868                         throw new Error( 'Failed to load script' );
869                 } );
872 // Skeleton user object, extended by the 'mediawiki.user' module.
874  * @namespace mw.user
875  * @ignore
876  */
877 mw.user = {
878         /**
879          * Map of user preferences and their values.
880          *
881          * @type {mw.Map}
882          */
883         options: new mw.Map(),
884         /**
885          * Map of retrieved user tokens.
886          *
887          * @type {mw.Map}
888          */
889         tokens: new mw.Map()
892 mw.user.options.set( require( './user.json' ) );
894 // Process callbacks for modern browsers (Grade A) that require modules.
895 const queue = window.RLQ;
896 // Replace temporary RLQ implementation from startup.js with the
897 // final implementation that also processes callbacks that can
898 // require modules. It must also support late arrivals of
899 // plain callbacks. (T208093)
900 window.RLQ = {
901         push: function ( entry ) {
902                 if ( typeof entry === 'function' ) {
903                         entry();
904                 } else {
905                         mw.loader.using( entry[ 0 ], entry[ 1 ] );
906                 }
907         }
909 while ( queue[ 0 ] ) {
910         window.RLQ.push( queue.shift() );
914  * Replace document.write/writeln with basic html parsing that appends
915  * to the `<body>` to avoid blanking pages. Added JavaScript will not run.
917  * @ignore
918  * @deprecated since 1.26
919  */
920 [ 'write', 'writeln' ].forEach( ( func ) => {
921         mw.log.deprecate( document, func, function () {
922                 $( document.body ).append( $.parseHTML( slice.call( arguments ).join( '' ) ) );
923         }, 'Use jQuery or mw.loader.load instead.', 'document.' + func );
924 } );
926 // Load other files in the package
927 require( './errorLogger.js' );