]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - resources/src/mediawiki/mediawiki.js
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / resources / src / mediawiki / mediawiki.js
1 /**
2  * Base library for MediaWiki.
3  *
4  * Exposed globally as `mediaWiki` with `mw` as shortcut.
5  *
6  * @class mw
7  * @alternateClassName mediaWiki
8  * @singleton
9  */
10
11 /* global mwNow */
12 /* eslint-disable no-use-before-define */
13
14 ( function ( $ ) {
15         'use strict';
16
17         var mw, StringSet, log,
18                 hasOwn = Object.prototype.hasOwnProperty,
19                 slice = Array.prototype.slice,
20                 trackCallbacks = $.Callbacks( 'memory' ),
21                 trackHandlers = [],
22                 trackQueue = [];
23
24         /**
25          * FNV132 hash function
26          *
27          * This function implements the 32-bit version of FNV-1.
28          * It is equivalent to hash( 'fnv132', ... ) in PHP, except
29          * its output is base 36 rather than hex.
30          * See <https://en.wikipedia.org/wiki/FNV_hash_function>
31          *
32          * @private
33          * @param {string} str String to hash
34          * @return {string} hash as an seven-character base 36 string
35          */
36         function fnv132( str ) {
37                 /* eslint-disable no-bitwise */
38                 var hash = 0x811C9DC5,
39                         i;
40
41                 for ( i = 0; i < str.length; i++ ) {
42                         hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
43                         hash ^= str.charCodeAt( i );
44                 }
45
46                 hash = ( hash >>> 0 ).toString( 36 );
47                 while ( hash.length < 7 ) {
48                         hash = '0' + hash;
49                 }
50
51                 return hash;
52                 /* eslint-enable no-bitwise */
53         }
54
55         function defineFallbacks() {
56                 // <https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set>
57                 StringSet = window.Set || ( function () {
58                         /**
59                          * @private
60                          * @class
61                          */
62                         function StringSet() {
63                                 this.set = {};
64                         }
65                         StringSet.prototype.add = function ( value ) {
66                                 this.set[ value ] = true;
67                         };
68                         StringSet.prototype.has = function ( value ) {
69                                 return hasOwn.call( this.set, value );
70                         };
71                         return StringSet;
72                 }() );
73         }
74
75         /**
76          * Create an object that can be read from or written to via methods that allow
77          * interaction both with single and multiple properties at once.
78          *
79          * @private
80          * @class mw.Map
81          *
82          * @constructor
83          * @param {boolean} [global=false] Whether to synchronise =values to the global
84          *  window object (for backwards-compatibility with mw.config; T72470). Values are
85          *  copied in one direction only. Changes to globals do not reflect in the map.
86          */
87         function Map( global ) {
88                 this.values = {};
89                 if ( global === true ) {
90                         // Override #set to also set the global variable
91                         this.set = function ( selection, value ) {
92                                 var s;
93
94                                 if ( $.isPlainObject( selection ) ) {
95                                         for ( s in selection ) {
96                                                 setGlobalMapValue( this, s, selection[ s ] );
97                                         }
98                                         return true;
99                                 }
100                                 if ( typeof selection === 'string' && arguments.length ) {
101                                         setGlobalMapValue( this, selection, value );
102                                         return true;
103                                 }
104                                 return false;
105                         };
106                 }
107         }
108
109         /**
110          * Alias property to the global object.
111          *
112          * @private
113          * @static
114          * @param {mw.Map} map
115          * @param {string} key
116          * @param {Mixed} value
117          */
118         function setGlobalMapValue( map, key, value ) {
119                 map.values[ key ] = value;
120                 log.deprecate(
121                         window,
122                         key,
123                         value,
124                         // Deprecation notice for mw.config globals (T58550, T72470)
125                         map === mw.config && 'Use mw.config instead.'
126                 );
127         }
128
129         Map.prototype = {
130                 constructor: Map,
131
132                 /**
133                  * Get the value of one or more keys.
134                  *
135                  * If called with no arguments, all values are returned.
136                  *
137                  * @param {string|Array} [selection] Key or array of keys to retrieve values for.
138                  * @param {Mixed} [fallback=null] Value for keys that don't exist.
139                  * @return {Mixed|Object|null} If selection was a string, returns the value,
140                  *  If selection was an array, returns an object of key/values.
141                  *  If no selection is passed, a new object with all key/values is returned.
142                  */
143                 get: function ( selection, fallback ) {
144                         var results, i;
145                         fallback = arguments.length > 1 ? fallback : null;
146
147                         if ( Array.isArray( selection ) ) {
148                                 results = {};
149                                 for ( i = 0; i < selection.length; i++ ) {
150                                         if ( typeof selection[ i ] === 'string' ) {
151                                                 results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ?
152                                                         this.values[ selection[ i ] ] :
153                                                         fallback;
154                                         }
155                                 }
156                                 return results;
157                         }
158
159                         if ( typeof selection === 'string' ) {
160                                 return hasOwn.call( this.values, selection ) ?
161                                         this.values[ selection ] :
162                                         fallback;
163                         }
164
165                         if ( selection === undefined ) {
166                                 results = {};
167                                 for ( i in this.values ) {
168                                         results[ i ] = this.values[ i ];
169                                 }
170                                 return results;
171                         }
172
173                         // Invalid selection key
174                         return fallback;
175                 },
176
177                 /**
178                  * Set one or more key/value pairs.
179                  *
180                  * @param {string|Object} selection Key to set value for, or object mapping keys to values
181                  * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
182                  * @return {boolean} True on success, false on failure
183                  */
184                 set: function ( selection, value ) {
185                         var s;
186
187                         if ( $.isPlainObject( selection ) ) {
188                                 for ( s in selection ) {
189                                         this.values[ s ] = selection[ s ];
190                                 }
191                                 return true;
192                         }
193                         if ( typeof selection === 'string' && arguments.length > 1 ) {
194                                 this.values[ selection ] = value;
195                                 return true;
196                         }
197                         return false;
198                 },
199
200                 /**
201                  * Check if one or more keys exist.
202                  *
203                  * @param {Mixed} selection Key or array of keys to check
204                  * @return {boolean} True if the key(s) exist
205                  */
206                 exists: function ( selection ) {
207                         var i;
208                         if ( Array.isArray( selection ) ) {
209                                 for ( i = 0; i < selection.length; i++ ) {
210                                         if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) {
211                                                 return false;
212                                         }
213                                 }
214                                 return true;
215                         }
216                         return typeof selection === 'string' && hasOwn.call( this.values, selection );
217                 }
218         };
219
220         /**
221          * Object constructor for messages.
222          *
223          * Similar to the Message class in MediaWiki PHP.
224          *
225          * Format defaults to 'text'.
226          *
227          *     @example
228          *
229          *     var obj, str;
230          *     mw.messages.set( {
231          *         'hello': 'Hello world',
232          *         'hello-user': 'Hello, $1!',
233          *         'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
234          *     } );
235          *
236          *     obj = new mw.Message( mw.messages, 'hello' );
237          *     mw.log( obj.text() );
238          *     // Hello world
239          *
240          *     obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
241          *     mw.log( obj.text() );
242          *     // Hello, John Doe!
243          *
244          *     obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
245          *     mw.log( obj.text() );
246          *     // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
247          *
248          *     // Using mw.message shortcut
249          *     obj = mw.message( 'hello-user', 'John Doe' );
250          *     mw.log( obj.text() );
251          *     // Hello, John Doe!
252          *
253          *     // Using mw.msg shortcut
254          *     str = mw.msg( 'hello-user', 'John Doe' );
255          *     mw.log( str );
256          *     // Hello, John Doe!
257          *
258          *     // Different formats
259          *     obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
260          *
261          *     obj.format = 'text';
262          *     str = obj.toString();
263          *     // Same as:
264          *     str = obj.text();
265          *
266          *     mw.log( str );
267          *     // Hello, John "Wiki" <3 Doe!
268          *
269          *     mw.log( obj.escaped() );
270          *     // Hello, John &quot;Wiki&quot; &lt;3 Doe!
271          *
272          * @class mw.Message
273          *
274          * @constructor
275          * @param {mw.Map} map Message store
276          * @param {string} key
277          * @param {Array} [parameters]
278          */
279         function Message( map, key, parameters ) {
280                 this.format = 'text';
281                 this.map = map;
282                 this.key = key;
283                 this.parameters = parameters === undefined ? [] : slice.call( parameters );
284                 return this;
285         }
286
287         Message.prototype = {
288                 /**
289                  * Get parsed contents of the message.
290                  *
291                  * The default parser does simple $N replacements and nothing else.
292                  * This may be overridden to provide a more complex message parser.
293                  * The primary override is in the mediawiki.jqueryMsg module.
294                  *
295                  * This function will not be called for nonexistent messages.
296                  *
297                  * @return {string} Parsed message
298                  */
299                 parser: function () {
300                         return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
301                 },
302
303                 // eslint-disable-next-line valid-jsdoc
304                 /**
305                  * Add (does not replace) parameters for `$N` placeholder values.
306                  *
307                  * @param {Array} parameters
308                  * @chainable
309                  */
310                 params: function ( parameters ) {
311                         var i;
312                         for ( i = 0; i < parameters.length; i++ ) {
313                                 this.parameters.push( parameters[ i ] );
314                         }
315                         return this;
316                 },
317
318                 /**
319                  * Convert message object to its string form based on current format.
320                  *
321                  * @return {string} Message as a string in the current form, or `<key>` if key
322                  *  does not exist.
323                  */
324                 toString: function () {
325                         var text;
326
327                         if ( !this.exists() ) {
328                                 // Use ⧼key⧽ as text if key does not exist
329                                 // Err on the side of safety, ensure that the output
330                                 // is always html safe in the event the message key is
331                                 // missing, since in that case its highly likely the
332                                 // message key is user-controlled.
333                                 // '⧼' is used instead of '<' to side-step any
334                                 // double-escaping issues.
335                                 // (Keep synchronised with Message::toString() in PHP.)
336                                 return '⧼' + mw.html.escape( this.key ) + '⧽';
337                         }
338
339                         if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
340                                 text = this.parser();
341                         }
342
343                         if ( this.format === 'escaped' ) {
344                                 text = this.parser();
345                                 text = mw.html.escape( text );
346                         }
347
348                         return text;
349                 },
350
351                 /**
352                  * Change format to 'parse' and convert message to string
353                  *
354                  * If jqueryMsg is loaded, this parses the message text from wikitext
355                  * (where supported) to HTML
356                  *
357                  * Otherwise, it is equivalent to plain.
358                  *
359                  * @return {string} String form of parsed message
360                  */
361                 parse: function () {
362                         this.format = 'parse';
363                         return this.toString();
364                 },
365
366                 /**
367                  * Change format to 'plain' and convert message to string
368                  *
369                  * This substitutes parameters, but otherwise does not change the
370                  * message text.
371                  *
372                  * @return {string} String form of plain message
373                  */
374                 plain: function () {
375                         this.format = 'plain';
376                         return this.toString();
377                 },
378
379                 /**
380                  * Change format to 'text' and convert message to string
381                  *
382                  * If jqueryMsg is loaded, {{-transformation is done where supported
383                  * (such as {{plural:}}, {{gender:}}, {{int:}}).
384                  *
385                  * Otherwise, it is equivalent to plain
386                  *
387                  * @return {string} String form of text message
388                  */
389                 text: function () {
390                         this.format = 'text';
391                         return this.toString();
392                 },
393
394                 /**
395                  * Change the format to 'escaped' and convert message to string
396                  *
397                  * This is equivalent to using the 'text' format (see #text), then
398                  * HTML-escaping the output.
399                  *
400                  * @return {string} String form of html escaped message
401                  */
402                 escaped: function () {
403                         this.format = 'escaped';
404                         return this.toString();
405                 },
406
407                 /**
408                  * Check if a message exists
409                  *
410                  * @see mw.Map#exists
411                  * @return {boolean}
412                  */
413                 exists: function () {
414                         return this.map.exists( this.key );
415                 }
416         };
417
418         defineFallbacks();
419
420         /* eslint-disable no-console */
421         log = ( function () {
422                 /**
423                  * Write a verbose message to the browser's console in debug mode.
424                  *
425                  * This method is mainly intended for verbose logging. It is a no-op in production mode.
426                  * In ResourceLoader debug mode, it will use the browser's console if available, with
427                  * fallback to creating a console interface in the DOM and logging messages there.
428                  *
429                  * See {@link mw.log} for other logging methods.
430                  *
431                  * @member mw
432                  * @param {...string} msg Messages to output to console.
433                  */
434                 var log = function () {},
435                         console = window.console;
436
437                 // Note: Keep list of methods in sync with restoration in mediawiki.log.js
438                 // when adding or removing mw.log methods below!
439
440                 /**
441                  * Collection of methods to help log messages to the console.
442                  *
443                  * @class mw.log
444                  * @singleton
445                  */
446
447                 /**
448                  * Write a message to the browser console's warning channel.
449                  *
450                  * This method is a no-op in browsers that don't implement the Console API.
451                  *
452                  * @param {...string} msg Messages to output to console
453                  */
454                 log.warn = console && console.warn && Function.prototype.bind ?
455                         Function.prototype.bind.call( console.warn, console ) :
456                         $.noop;
457
458                 /**
459                  * Write a message to the browser console's error channel.
460                  *
461                  * Most browsers also print a stacktrace when calling this method if the
462                  * argument is an Error object.
463                  *
464                  * This method is a no-op in browsers that don't implement the Console API.
465                  *
466                  * @since 1.26
467                  * @param {Error|...string} msg Messages to output to console
468                  */
469                 log.error = console && console.error && Function.prototype.bind ?
470                         Function.prototype.bind.call( console.error, console ) :
471                         $.noop;
472
473                 /**
474                  * Create a property on a host object that, when accessed, will produce
475                  * a deprecation warning in the console.
476                  *
477                  * @param {Object} obj Host object of deprecated property
478                  * @param {string} key Name of property to create in `obj`
479                  * @param {Mixed} val The value this property should return when accessed
480                  * @param {string} [msg] Optional text to include in the deprecation message
481                  * @param {string} [logName=key] Optional custom name for the feature.
482                  *  This is used instead of `key` in the message and `mw.deprecate` tracking.
483                  */
484                 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
485                         obj[ key ] = val;
486                 } : function ( obj, key, val, msg, logName ) {
487                         var logged = new StringSet();
488                         logName = logName || key;
489                         msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
490                         function uniqueTrace() {
491                                 var trace = new Error().stack;
492                                 if ( logged.has( trace ) ) {
493                                         return false;
494                                 }
495                                 logged.add( trace );
496                                 return true;
497                         }
498                         // Support: Safari 5.0
499                         // Throws "not supported on DOM Objects" for Node or Element objects (incl. document)
500                         // Safari 4.0 doesn't have this method, and it was fixed in Safari 5.1.
501                         try {
502                                 Object.defineProperty( obj, key, {
503                                         configurable: true,
504                                         enumerable: true,
505                                         get: function () {
506                                                 if ( uniqueTrace() ) {
507                                                         mw.track( 'mw.deprecate', logName );
508                                                         mw.log.warn( msg );
509                                                 }
510                                                 return val;
511                                         },
512                                         set: function ( newVal ) {
513                                                 if ( uniqueTrace() ) {
514                                                         mw.track( 'mw.deprecate', logName );
515                                                         mw.log.warn( msg );
516                                                 }
517                                                 val = newVal;
518                                         }
519                                 } );
520                         } catch ( err ) {
521                                 obj[ key ] = val;
522                         }
523                 };
524
525                 return log;
526         }() );
527         /* eslint-enable no-console */
528
529         /**
530          * @class mw
531          */
532         mw = {
533                 redefineFallbacksForTest: function () {
534                         if ( !window.QUnit ) {
535                                 throw new Error( 'Reset not allowed outside unit tests' );
536                         }
537                         defineFallbacks();
538                 },
539
540                 /**
541                  * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
542                  *
543                  * On browsers that implement the Navigation Timing API, this function will produce floating-point
544                  * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
545                  * it will fall back to using `Date`.
546                  *
547                  * @return {number} Current time
548                  */
549                 now: mwNow,
550                 // mwNow is defined in startup.js
551
552                 /**
553                  * Format a string. Replace $1, $2 ... $N with positional arguments.
554                  *
555                  * Used by Message#parser().
556                  *
557                  * @since 1.25
558                  * @param {string} formatString Format string
559                  * @param {...Mixed} parameters Values for $N replacements
560                  * @return {string} Formatted string
561                  */
562                 format: function ( formatString ) {
563                         var parameters = slice.call( arguments, 1 );
564                         return formatString.replace( /\$(\d+)/g, function ( str, match ) {
565                                 var index = parseInt( match, 10 ) - 1;
566                                 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
567                         } );
568                 },
569
570                 /**
571                  * Track an analytic event.
572                  *
573                  * This method provides a generic means for MediaWiki JavaScript code to capture state
574                  * information for analysis. Each logged event specifies a string topic name that describes
575                  * the kind of event that it is. Topic names consist of dot-separated path components,
576                  * arranged from most general to most specific. Each path component should have a clear and
577                  * well-defined purpose.
578                  *
579                  * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
580                  * events that match their subcription, including those that fired before the handler was
581                  * bound.
582                  *
583                  * @param {string} topic Topic name
584                  * @param {Object} [data] Data describing the event, encoded as an object
585                  */
586                 track: function ( topic, data ) {
587                         trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
588                         trackCallbacks.fire( trackQueue );
589                 },
590
591                 /**
592                  * Register a handler for subset of analytic events, specified by topic.
593                  *
594                  * Handlers will be called once for each tracked event, including any events that fired before the
595                  * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
596                  * the exact time at which the event fired, a string 'topic' property naming the event, and a
597                  * 'data' property which is an object of event-specific data. The event topic and event data are
598                  * also passed to the callback as the first and second arguments, respectively.
599                  *
600                  * @param {string} topic Handle events whose name starts with this string prefix
601                  * @param {Function} callback Handler to call for each matching tracked event
602                  * @param {string} callback.topic
603                  * @param {Object} [callback.data]
604                  */
605                 trackSubscribe: function ( topic, callback ) {
606                         var seen = 0;
607                         function handler( trackQueue ) {
608                                 var event;
609                                 for ( ; seen < trackQueue.length; seen++ ) {
610                                         event = trackQueue[ seen ];
611                                         if ( event.topic.indexOf( topic ) === 0 ) {
612                                                 callback.call( event, event.topic, event.data );
613                                         }
614                                 }
615                         }
616
617                         trackHandlers.push( [ handler, callback ] );
618
619                         trackCallbacks.add( handler );
620                 },
621
622                 /**
623                  * Stop handling events for a particular handler
624                  *
625                  * @param {Function} callback
626                  */
627                 trackUnsubscribe: function ( callback ) {
628                         trackHandlers = $.grep( trackHandlers, function ( fns ) {
629                                 if ( fns[ 1 ] === callback ) {
630                                         trackCallbacks.remove( fns[ 0 ] );
631                                         // Ensure the tuple is removed to avoid holding on to closures
632                                         return false;
633                                 }
634                                 return true;
635                         } );
636                 },
637
638                 // Expose Map constructor
639                 Map: Map,
640
641                 // Expose Message constructor
642                 Message: Message,
643
644                 /**
645                  * Map of configuration values.
646                  *
647                  * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
648                  * on mediawiki.org.
649                  *
650                  * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
651                  * global `window` object.
652                  *
653                  * @property {mw.Map} config
654                  */
655                 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
656                 config: null,
657
658                 /**
659                  * Empty object for third-party libraries, for cases where you don't
660                  * want to add a new global, or the global is bad and needs containment
661                  * or wrapping.
662                  *
663                  * @property
664                  */
665                 libs: {},
666
667                 /**
668                  * Access container for deprecated functionality that can be moved from
669                  * from their legacy location and attached to this object (e.g. a global
670                  * function that is deprecated and as stop-gap can be exposed through here).
671                  *
672                  * This was reserved for future use but never ended up being used.
673                  *
674                  * @deprecated since 1.22 Let deprecated identifiers keep their original name
675                  *  and use mw.log#deprecate to create an access container for tracking.
676                  * @property
677                  */
678                 legacy: {},
679
680                 /**
681                  * Store for messages.
682                  *
683                  * @property {mw.Map}
684                  */
685                 messages: new Map(),
686
687                 /**
688                  * Store for templates associated with a module.
689                  *
690                  * @property {mw.Map}
691                  */
692                 templates: new Map(),
693
694                 /**
695                  * Get a message object.
696                  *
697                  * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
698                  *
699                  * @see mw.Message
700                  * @param {string} key Key of message to get
701                  * @param {...Mixed} parameters Values for $N replacements
702                  * @return {mw.Message}
703                  */
704                 message: function ( key ) {
705                         var parameters = slice.call( arguments, 1 );
706                         return new Message( mw.messages, key, parameters );
707                 },
708
709                 /**
710                  * Get a message string using the (default) 'text' format.
711                  *
712                  * Shortcut for `mw.message( key, parameters... ).text()`.
713                  *
714                  * @see mw.Message
715                  * @param {string} key Key of message to get
716                  * @param {...Mixed} parameters Values for $N replacements
717                  * @return {string}
718                  */
719                 msg: function () {
720                         return mw.message.apply( mw.message, arguments ).toString();
721                 },
722
723                 // Expose mw.log
724                 log: log,
725
726                 /**
727                  * Client for ResourceLoader server end point.
728                  *
729                  * This client is in charge of maintaining the module registry and state
730                  * machine, initiating network (batch) requests for loading modules, as
731                  * well as dependency resolution and execution of source code.
732                  *
733                  * For more information, refer to
734                  * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
735                  *
736                  * @class mw.loader
737                  * @singleton
738                  */
739                 loader: ( function () {
740
741                         /**
742                          * Fired via mw.track on various resource loading errors.
743                          *
744                          * @event resourceloader_exception
745                          * @param {Error|Mixed} e The error that was thrown. Almost always an Error
746                          *   object, but in theory module code could manually throw something else, and that
747                          *   might also end up here.
748                          * @param {string} [module] Name of the module which caused the error. Omitted if the
749                          *   error is not module-related or the module cannot be easily identified due to
750                          *   batched handling.
751                          * @param {string} source Source of the error. Possible values:
752                          *
753                          *   - style: stylesheet error (only affects old IE where a special style loading method
754                          *     is used)
755                          *   - load-callback: exception thrown by user callback
756                          *   - module-execute: exception thrown by module code
757                          *   - resolve: failed to sort dependencies for a module in mw.loader.load
758                          *   - store-eval: could not evaluate module code cached in localStorage
759                          *   - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
760                          *   - store-localstorage-json: JSON conversion error in mw.loader.store.set
761                          *   - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
762                          */
763
764                         /**
765                          * Fired via mw.track on resource loading error conditions.
766                          *
767                          * @event resourceloader_assert
768                          * @param {string} source Source of the error. Possible values:
769                          *
770                          *   - bug-T59567: failed to cache script due to an Opera function -> string conversion
771                          *     bug; see <https://phabricator.wikimedia.org/T59567> for details
772                          */
773
774                         /**
775                          * Mapping of registered modules.
776                          *
777                          * See #implement and #execute for exact details on support for script, style and messages.
778                          *
779                          * Format:
780                          *
781                          *     {
782                          *         'moduleName': {
783                          *             // From mw.loader.register()
784                          *             'version': '########' (hash)
785                          *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
786                          *             'group': 'somegroup', (or) null
787                          *             'source': 'local', (or) 'anotherwiki'
788                          *             'skip': 'return !!window.Example', (or) null
789                          *             'module': export Object
790                          *
791                          *             // Set from execute() or mw.loader.state()
792                          *             'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
793                          *
794                          *             // Optionally added at run-time by mw.loader.implement()
795                          *             'skipped': true
796                          *             'script': closure, array of urls, or string
797                          *             'style': { ... } (see #execute)
798                          *             'messages': { 'key': 'value', ... }
799                          *         }
800                          *     }
801                          *
802                          * State machine:
803                          *
804                          * - `registered`:
805                          *    The module is known to the system but not yet required.
806                          *    Meta data is registered via mw.loader#register. Calls to that method are
807                          *    generated server-side by the startup module.
808                          * - `loading`:
809                          *    The module was required through mw.loader (either directly or as dependency of
810                          *    another module). The client will fetch module contents from the server.
811                          *    The contents are then stashed in the registry via mw.loader#implement.
812                          * - `loaded`:
813                          *    The module has been loaded from the server and stashed via mw.loader#implement.
814                          *    If the module has no more dependencies in-flight, the module will be executed
815                          *    immediately. Otherwise execution is deferred, controlled via #handlePending.
816                          * - `executing`:
817                          *    The module is being executed.
818                          * - `ready`:
819                          *    The module has been successfully executed.
820                          * - `error`:
821                          *    The module (or one of its dependencies) produced an error during execution.
822                          * - `missing`:
823                          *    The module was registered client-side and requested, but the server denied knowledge
824                          *    of the module's existence.
825                          *
826                          * @property
827                          * @private
828                          */
829                         var registry = {},
830                                 // Mapping of sources, keyed by source-id, values are strings.
831                                 //
832                                 // Format:
833                                 //
834                                 //     {
835                                 //         'sourceId': 'http://example.org/w/load.php'
836                                 //     }
837                                 //
838                                 sources = {},
839
840                                 // For queueModuleScript()
841                                 handlingPendingRequests = false,
842                                 pendingRequests = [],
843
844                                 // List of modules to be loaded
845                                 queue = [],
846
847                                 /**
848                                  * List of callback jobs waiting for modules to be ready.
849                                  *
850                                  * Jobs are created by #enqueue() and run by #handlePending().
851                                  *
852                                  * Typically when a job is created for a module, the job's dependencies contain
853                                  * both the required module and all its recursive dependencies.
854                                  *
855                                  * Format:
856                                  *
857                                  *     {
858                                  *         'dependencies': [ module names ],
859                                  *         'ready': Function callback
860                                  *         'error': Function callback
861                                  *     }
862                                  *
863                                  * @property {Object[]} jobs
864                                  * @private
865                                  */
866                                 jobs = [],
867
868                                 // For getMarker()
869                                 marker = null,
870
871                                 // For addEmbeddedCSS()
872                                 cssBuffer = '',
873                                 cssBufferTimer = null,
874                                 cssCallbacks = $.Callbacks(),
875                                 rAF = window.requestAnimationFrame || setTimeout;
876
877                         function getMarker() {
878                                 if ( !marker ) {
879                                         // Cache
880                                         marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' );
881                                         if ( !marker ) {
882                                                 mw.log( 'Create <meta name="ResourceLoaderDynamicStyles"> dynamically' );
883                                                 marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' )[ 0 ];
884                                         }
885                                 }
886                                 return marker;
887                         }
888
889                         /**
890                          * Create a new style element and add it to the DOM.
891                          *
892                          * @private
893                          * @param {string} text CSS text
894                          * @param {Node} [nextNode] The element where the style tag
895                          *  should be inserted before
896                          * @return {HTMLElement} Reference to the created style element
897                          */
898                         function newStyleTag( text, nextNode ) {
899                                 var s = document.createElement( 'style' );
900
901                                 s.appendChild( document.createTextNode( text ) );
902                                 if ( nextNode && nextNode.parentNode ) {
903                                         nextNode.parentNode.insertBefore( s, nextNode );
904                                 } else {
905                                         document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
906                                 }
907
908                                 return s;
909                         }
910
911                         /**
912                          * Add a bit of CSS text to the current browser page.
913                          *
914                          * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
915                          * or create a new one based on whether the given `cssText` is safe for extension.
916                          *
917                          * @private
918                          * @param {string} [cssText=cssBuffer] If called without cssText,
919                          *  the internal buffer will be inserted instead.
920                          * @param {Function} [callback]
921                          */
922                         function addEmbeddedCSS( cssText, callback ) {
923                                 function fireCallbacks() {
924                                         var oldCallbacks = cssCallbacks;
925                                         // Reset cssCallbacks variable so it's not polluted by any calls to
926                                         // addEmbeddedCSS() from one of the callbacks (T105973)
927                                         cssCallbacks = $.Callbacks();
928                                         oldCallbacks.fire().empty();
929                                 }
930
931                                 if ( callback ) {
932                                         cssCallbacks.add( callback );
933                                 }
934
935                                 // Yield once before creating the <style> tag. This lets multiple stylesheets
936                                 // accumulate into one buffer, allowing us to reduce how often new stylesheets
937                                 // are inserted in the browser. Appending a stylesheet and waiting for the
938                                 // browser to repaint is fairly expensive. (T47810)
939                                 if ( cssText ) {
940                                         // Don't extend the buffer if the item needs its own stylesheet.
941                                         // Keywords like `@import` are only valid at the start of a stylesheet (T37562).
942                                         if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
943                                                 // Linebreak for somewhat distinguishable sections
944                                                 cssBuffer += '\n' + cssText;
945                                                 if ( !cssBufferTimer ) {
946                                                         cssBufferTimer = rAF( function () {
947                                                                 // Wrap in anonymous function that takes no arguments
948                                                                 // Support: Firefox < 13
949                                                                 // Firefox 12 has non-standard behaviour of passing a number
950                                                                 // as first argument to a setTimeout callback.
951                                                                 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
952                                                                 addEmbeddedCSS();
953                                                         } );
954                                                 }
955                                                 return;
956                                         }
957
958                                 // This is a scheduled flush for the buffer
959                                 } else {
960                                         cssBufferTimer = null;
961                                         cssText = cssBuffer;
962                                         cssBuffer = '';
963                                 }
964
965                                 $( newStyleTag( cssText, getMarker() ) );
966
967                                 fireCallbacks();
968                         }
969
970                         /**
971                          * @private
972                          * @param {Array} modules List of module names
973                          * @return {string} Hash of concatenated version hashes.
974                          */
975                         function getCombinedVersion( modules ) {
976                                 var hashes = modules.map( function ( module ) {
977                                         return registry[ module ].version;
978                                 } );
979                                 return fnv132( hashes.join( '' ) );
980                         }
981
982                         /**
983                          * Determine whether all dependencies are in state 'ready', which means we may
984                          * execute the module or job now.
985                          *
986                          * @private
987                          * @param {Array} modules Names of modules to be checked
988                          * @return {boolean} True if all modules are in state 'ready', false otherwise
989                          */
990                         function allReady( modules ) {
991                                 var i;
992                                 for ( i = 0; i < modules.length; i++ ) {
993                                         if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
994                                                 return false;
995                                         }
996                                 }
997                                 return true;
998                         }
999
1000                         /**
1001                          * Determine whether all dependencies are in state 'ready', which means we may
1002                          * execute the module or job now.
1003                          *
1004                          * @private
1005                          * @param {Array} modules Names of modules to be checked
1006                          * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
1007                          */
1008                         function anyFailed( modules ) {
1009                                 var i, state;
1010                                 for ( i = 0; i < modules.length; i++ ) {
1011                                         state = mw.loader.getState( modules[ i ] );
1012                                         if ( state === 'error' || state === 'missing' ) {
1013                                                 return true;
1014                                         }
1015                                 }
1016                                 return false;
1017                         }
1018
1019                         /**
1020                          * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1021                          * pending jobs and modules that depend upon this module. If the given module failed,
1022                          * propagate the 'error' state up the dependency tree. Otherwise, go ahead and execute
1023                          * all jobs/modules now having their dependencies satisfied.
1024                          *
1025                          * Jobs that depend on a failed module, will have their error callback ran (if any).
1026                          *
1027                          * @private
1028                          * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1029                          */
1030                         function handlePending( module ) {
1031                                 var j, job, hasErrors, m, stateChange;
1032
1033                                 if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
1034                                         // If the current module failed, mark all dependent modules also as failed.
1035                                         // Iterate until steady-state to propagate the error state upwards in the
1036                                         // dependency tree.
1037                                         do {
1038                                                 stateChange = false;
1039                                                 for ( m in registry ) {
1040                                                         if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
1041                                                                 if ( anyFailed( registry[ m ].dependencies ) ) {
1042                                                                         registry[ m ].state = 'error';
1043                                                                         stateChange = true;
1044                                                                 }
1045                                                         }
1046                                                 }
1047                                         } while ( stateChange );
1048                                 }
1049
1050                                 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1051                                 for ( j = 0; j < jobs.length; j++ ) {
1052                                         hasErrors = anyFailed( jobs[ j ].dependencies );
1053                                         if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
1054                                                 // All dependencies satisfied, or some have errors
1055                                                 job = jobs[ j ];
1056                                                 jobs.splice( j, 1 );
1057                                                 j -= 1;
1058                                                 try {
1059                                                         if ( hasErrors ) {
1060                                                                 if ( typeof job.error === 'function' ) {
1061                                                                         job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
1062                                                                 }
1063                                                         } else {
1064                                                                 if ( typeof job.ready === 'function' ) {
1065                                                                         job.ready();
1066                                                                 }
1067                                                         }
1068                                                 } catch ( e ) {
1069                                                         // A user-defined callback raised an exception.
1070                                                         // Swallow it to protect our state machine!
1071                                                         mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
1072                                                 }
1073                                         }
1074                                 }
1075
1076                                 if ( registry[ module ].state === 'ready' ) {
1077                                         // The current module became 'ready'. Set it in the module store, and recursively execute all
1078                                         // dependent modules that are loaded and now have all dependencies satisfied.
1079                                         mw.loader.store.set( module, registry[ module ] );
1080                                         for ( m in registry ) {
1081                                                 if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
1082                                                         execute( m );
1083                                                 }
1084                                         }
1085                                 }
1086                         }
1087
1088                         /**
1089                          * Resolve dependencies and detect circular references.
1090                          *
1091                          * @private
1092                          * @param {string} module Name of the top-level module whose dependencies shall be
1093                          *  resolved and sorted.
1094                          * @param {Array} resolved Returns a topological sort of the given module and its
1095                          *  dependencies, such that later modules depend on earlier modules. The array
1096                          *  contains the module names. If the array contains already some module names,
1097                          *  this function appends its result to the pre-existing array.
1098                          * @param {StringSet} [unresolved] Used to track the current dependency
1099                          *  chain, and to report loops in the dependency graph.
1100                          * @throws {Error} If any unregistered module or a dependency loop is encountered
1101                          */
1102                         function sortDependencies( module, resolved, unresolved ) {
1103                                 var i, deps, skip;
1104
1105                                 if ( !hasOwn.call( registry, module ) ) {
1106                                         throw new Error( 'Unknown dependency: ' + module );
1107                                 }
1108
1109                                 if ( registry[ module ].skip !== null ) {
1110                                         // eslint-disable-next-line no-new-func
1111                                         skip = new Function( registry[ module ].skip );
1112                                         registry[ module ].skip = null;
1113                                         if ( skip() ) {
1114                                                 registry[ module ].skipped = true;
1115                                                 registry[ module ].dependencies = [];
1116                                                 registry[ module ].state = 'ready';
1117                                                 handlePending( module );
1118                                                 return;
1119                                         }
1120                                 }
1121
1122                                 // Resolves dynamic loader function and replaces it with its own results
1123                                 if ( typeof registry[ module ].dependencies === 'function' ) {
1124                                         registry[ module ].dependencies = registry[ module ].dependencies();
1125                                         // Ensures the module's dependencies are always in an array
1126                                         if ( typeof registry[ module ].dependencies !== 'object' ) {
1127                                                 registry[ module ].dependencies = [ registry[ module ].dependencies ];
1128                                         }
1129                                 }
1130                                 if ( $.inArray( module, resolved ) !== -1 ) {
1131                                         // Module already resolved; nothing to do
1132                                         return;
1133                                 }
1134                                 // Create unresolved if not passed in
1135                                 if ( !unresolved ) {
1136                                         unresolved = new StringSet();
1137                                 }
1138                                 // Tracks down dependencies
1139                                 deps = registry[ module ].dependencies;
1140                                 for ( i = 0; i < deps.length; i++ ) {
1141                                         if ( $.inArray( deps[ i ], resolved ) === -1 ) {
1142                                                 if ( unresolved.has( deps[ i ] ) ) {
1143                                                         throw new Error( mw.format(
1144                                                                 'Circular reference detected: $1 -> $2',
1145                                                                 module,
1146                                                                 deps[ i ]
1147                                                         ) );
1148                                                 }
1149
1150                                                 unresolved.add( module );
1151                                                 sortDependencies( deps[ i ], resolved, unresolved );
1152                                         }
1153                                 }
1154                                 resolved.push( module );
1155                         }
1156
1157                         /**
1158                          * Get names of module that a module depends on, in their proper dependency order.
1159                          *
1160                          * @private
1161                          * @param {string[]} modules Array of string module names
1162                          * @return {Array} List of dependencies, including 'module'.
1163                          * @throws {Error} If an unregistered module or a dependency loop is encountered
1164                          */
1165                         function resolve( modules ) {
1166                                 var i, resolved = [];
1167                                 for ( i = 0; i < modules.length; i++ ) {
1168                                         sortDependencies( modules[ i ], resolved );
1169                                 }
1170                                 return resolved;
1171                         }
1172
1173                         /**
1174                          * Like #resolve(), except it will silently ignore modules that
1175                          * are missing or have missing dependencies.
1176                          *
1177                          * @private
1178                          * @param {string[]} modules Array of string module names
1179                          * @return {Array} List of dependencies.
1180                          */
1181                         function resolveStubbornly( modules ) {
1182                                 var i, saved, resolved = [];
1183                                 for ( i = 0; i < modules.length; i++ ) {
1184                                         saved = resolved.slice();
1185                                         try {
1186                                                 sortDependencies( modules[ i ], resolved );
1187                                         } catch ( err ) {
1188                                                 // This module is unknown or has unknown dependencies.
1189                                                 // Undo any incomplete resolutions made and keep going.
1190                                                 resolved = saved;
1191                                                 mw.track( 'resourceloader.exception', {
1192                                                         exception: err,
1193                                                         source: 'resolve'
1194                                                 } );
1195                                         }
1196                                 }
1197                                 return resolved;
1198                         }
1199
1200                         /**
1201                          * Load and execute a script.
1202                          *
1203                          * @private
1204                          * @param {string} src URL to script, will be used as the src attribute in the script tag
1205                          * @return {jQuery.Promise}
1206                          */
1207                         function addScript( src ) {
1208                                 return $.ajax( {
1209                                         url: src,
1210                                         dataType: 'script',
1211                                         // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1212                                         // XHR for a same domain request instead of <script>, which changes the request
1213                                         // headers (potentially missing a cache hit), and reduces caching in general
1214                                         // since browsers cache XHR much less (if at all). And XHR means we retrieve
1215                                         // text, so we'd need to $.globalEval, which then messes up line numbers.
1216                                         crossDomain: true,
1217                                         cache: true
1218                                 } );
1219                         }
1220
1221                         /**
1222                          * Queue the loading and execution of a script for a particular module.
1223                          *
1224                          * @private
1225                          * @param {string} src URL of the script
1226                          * @param {string} [moduleName] Name of currently executing module
1227                          * @return {jQuery.Promise}
1228                          */
1229                         function queueModuleScript( src, moduleName ) {
1230                                 var r = $.Deferred();
1231
1232                                 pendingRequests.push( function () {
1233                                         if ( moduleName && hasOwn.call( registry, moduleName ) ) {
1234                                                 // Emulate runScript() part of execute()
1235                                                 window.require = mw.loader.require;
1236                                                 window.module = registry[ moduleName ].module;
1237                                         }
1238                                         addScript( src ).always( function () {
1239                                                 // 'module.exports' should not persist after the file is executed to
1240                                                 // avoid leakage to unrelated code. 'require' should be kept, however,
1241                                                 // as asynchronous access to 'require' is allowed and expected. (T144879)
1242                                                 delete window.module;
1243                                                 r.resolve();
1244
1245                                                 // Start the next one (if any)
1246                                                 if ( pendingRequests[ 0 ] ) {
1247                                                         pendingRequests.shift()();
1248                                                 } else {
1249                                                         handlingPendingRequests = false;
1250                                                 }
1251                                         } );
1252                                 } );
1253                                 if ( !handlingPendingRequests && pendingRequests[ 0 ] ) {
1254                                         handlingPendingRequests = true;
1255                                         pendingRequests.shift()();
1256                                 }
1257                                 return r.promise();
1258                         }
1259
1260                         /**
1261                          * Utility function for execute()
1262                          *
1263                          * @ignore
1264                          * @param {string} [media] Media attribute
1265                          * @param {string} url URL
1266                          */
1267                         function addLink( media, url ) {
1268                                 var el = document.createElement( 'link' );
1269
1270                                 el.rel = 'stylesheet';
1271                                 if ( media && media !== 'all' ) {
1272                                         el.media = media;
1273                                 }
1274                                 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1275                                 // see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
1276                                 el.href = url;
1277
1278                                 $( getMarker() ).before( el );
1279                         }
1280
1281                         /**
1282                          * Executes a loaded module, making it ready to use
1283                          *
1284                          * @private
1285                          * @param {string} module Module name to execute
1286                          */
1287                         function execute( module ) {
1288                                 var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
1289                                         cssHandlesRegistered = false;
1290
1291                                 if ( !hasOwn.call( registry, module ) ) {
1292                                         throw new Error( 'Module has not been registered yet: ' + module );
1293                                 }
1294                                 if ( registry[ module ].state !== 'loaded' ) {
1295                                         throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
1296                                 }
1297
1298                                 registry[ module ].state = 'executing';
1299
1300                                 runScript = function () {
1301                                         var script, markModuleReady, nestedAddScript;
1302
1303                                         script = registry[ module ].script;
1304                                         markModuleReady = function () {
1305                                                 registry[ module ].state = 'ready';
1306                                                 handlePending( module );
1307                                         };
1308                                         nestedAddScript = function ( arr, callback, i ) {
1309                                                 // Recursively call queueModuleScript() in its own callback
1310                                                 // for each element of arr.
1311                                                 if ( i >= arr.length ) {
1312                                                         // We're at the end of the array
1313                                                         callback();
1314                                                         return;
1315                                                 }
1316
1317                                                 queueModuleScript( arr[ i ], module ).always( function () {
1318                                                         nestedAddScript( arr, callback, i + 1 );
1319                                                 } );
1320                                         };
1321
1322                                         try {
1323                                                 if ( Array.isArray( script ) ) {
1324                                                         nestedAddScript( script, markModuleReady, 0 );
1325                                                 } else if ( typeof script === 'function' ) {
1326                                                         // Pass jQuery twice so that the signature of the closure which wraps
1327                                                         // the script can bind both '$' and 'jQuery'.
1328                                                         script( $, $, mw.loader.require, registry[ module ].module );
1329                                                         markModuleReady();
1330
1331                                                 } else if ( typeof script === 'string' ) {
1332                                                         // Site and user modules are legacy scripts that run in the global scope.
1333                                                         // This is transported as a string instead of a function to avoid needing
1334                                                         // to use string manipulation to undo the function wrapper.
1335                                                         $.globalEval( script );
1336                                                         markModuleReady();
1337
1338                                                 } else {
1339                                                         // Module without script
1340                                                         markModuleReady();
1341                                                 }
1342                                         } catch ( e ) {
1343                                                 // Use mw.track instead of mw.log because these errors are common in production mode
1344                                                 // (e.g. undefined variable), and mw.log is only enabled in debug mode.
1345                                                 registry[ module ].state = 'error';
1346                                                 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
1347                                                 handlePending( module );
1348                                         }
1349                                 };
1350
1351                                 // Add localizations to message system
1352                                 if ( registry[ module ].messages ) {
1353                                         mw.messages.set( registry[ module ].messages );
1354                                 }
1355
1356                                 // Initialise templates
1357                                 if ( registry[ module ].templates ) {
1358                                         mw.templates.set( module, registry[ module ].templates );
1359                                 }
1360
1361                                 // Make sure we don't run the scripts until all stylesheet insertions have completed.
1362                                 ( function () {
1363                                         var pending = 0;
1364                                         checkCssHandles = function () {
1365                                                 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1366                                                 // one of the cssHandles is fired while we're still creating more handles.
1367                                                 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1368                                                         if ( module === 'user' ) {
1369                                                                 // Implicit dependency on the site module. Not real dependency because
1370                                                                 // it should run after 'site' regardless of whether it succeeds or fails.
1371                                                                 mw.loader.using( [ 'site' ] ).always( runScript );
1372                                                         } else {
1373                                                                 runScript();
1374                                                         }
1375                                                         runScript = undefined; // Revoke
1376                                                 }
1377                                         };
1378                                         cssHandle = function () {
1379                                                 var check = checkCssHandles;
1380                                                 pending++;
1381                                                 return function () {
1382                                                         if ( check ) {
1383                                                                 pending--;
1384                                                                 check();
1385                                                                 check = undefined; // Revoke
1386                                                         }
1387                                                 };
1388                                         };
1389                                 }() );
1390
1391                                 // Process styles (see also mw.loader.implement)
1392                                 // * back-compat: { <media>: css }
1393                                 // * back-compat: { <media>: [url, ..] }
1394                                 // * { "css": [css, ..] }
1395                                 // * { "url": { <media>: [url, ..] } }
1396                                 if ( registry[ module ].style ) {
1397                                         for ( key in registry[ module ].style ) {
1398                                                 value = registry[ module ].style[ key ];
1399                                                 media = undefined;
1400
1401                                                 if ( key !== 'url' && key !== 'css' ) {
1402                                                         // Backwards compatibility, key is a media-type
1403                                                         if ( typeof value === 'string' ) {
1404                                                                 // back-compat: { <media>: css }
1405                                                                 // Ignore 'media' because it isn't supported (nor was it used).
1406                                                                 // Strings are pre-wrapped in "@media". The media-type was just ""
1407                                                                 // (because it had to be set to something).
1408                                                                 // This is one of the reasons why this format is no longer used.
1409                                                                 addEmbeddedCSS( value, cssHandle() );
1410                                                         } else {
1411                                                                 // back-compat: { <media>: [url, ..] }
1412                                                                 media = key;
1413                                                                 key = 'bc-url';
1414                                                         }
1415                                                 }
1416
1417                                                 // Array of css strings in key 'css',
1418                                                 // or back-compat array of urls from media-type
1419                                                 if ( Array.isArray( value ) ) {
1420                                                         for ( i = 0; i < value.length; i++ ) {
1421                                                                 if ( key === 'bc-url' ) {
1422                                                                         // back-compat: { <media>: [url, ..] }
1423                                                                         addLink( media, value[ i ] );
1424                                                                 } else if ( key === 'css' ) {
1425                                                                         // { "css": [css, ..] }
1426                                                                         addEmbeddedCSS( value[ i ], cssHandle() );
1427                                                                 }
1428                                                         }
1429                                                 // Not an array, but a regular object
1430                                                 // Array of urls inside media-type key
1431                                                 } else if ( typeof value === 'object' ) {
1432                                                         // { "url": { <media>: [url, ..] } }
1433                                                         for ( media in value ) {
1434                                                                 urls = value[ media ];
1435                                                                 for ( i = 0; i < urls.length; i++ ) {
1436                                                                         addLink( media, urls[ i ] );
1437                                                                 }
1438                                                         }
1439                                                 }
1440                                         }
1441                                 }
1442
1443                                 // Kick off.
1444                                 cssHandlesRegistered = true;
1445                                 checkCssHandles();
1446                         }
1447
1448                         /**
1449                          * Add one or more modules to the module load queue.
1450                          *
1451                          * See also #work().
1452                          *
1453                          * @private
1454                          * @param {string|string[]} dependencies Module name or array of string module names
1455                          * @param {Function} [ready] Callback to execute when all dependencies are ready
1456                          * @param {Function} [error] Callback to execute when any dependency fails
1457                          */
1458                         function enqueue( dependencies, ready, error ) {
1459                                 // Allow calling by single module name
1460                                 if ( typeof dependencies === 'string' ) {
1461                                         dependencies = [ dependencies ];
1462                                 }
1463
1464                                 // Add ready and error callbacks if they were given
1465                                 if ( ready !== undefined || error !== undefined ) {
1466                                         jobs.push( {
1467                                                 // Narrow down the list to modules that are worth waiting for
1468                                                 dependencies: $.grep( dependencies, function ( module ) {
1469                                                         var state = mw.loader.getState( module );
1470                                                         return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
1471                                                 } ),
1472                                                 ready: ready,
1473                                                 error: error
1474                                         } );
1475                                 }
1476
1477                                 $.each( dependencies, function ( idx, module ) {
1478                                         var state = mw.loader.getState( module );
1479                                         // Only queue modules that are still in the initial 'registered' state
1480                                         // (not ones already loading, ready or error).
1481                                         if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
1482                                                 // Private modules must be embedded in the page. Don't bother queuing
1483                                                 // these as the server will deny them anyway (T101806).
1484                                                 if ( registry[ module ].group === 'private' ) {
1485                                                         registry[ module ].state = 'error';
1486                                                         handlePending( module );
1487                                                         return;
1488                                                 }
1489                                                 queue.push( module );
1490                                         }
1491                                 } );
1492
1493                                 mw.loader.work();
1494                         }
1495
1496                         function sortQuery( o ) {
1497                                 var key,
1498                                         sorted = {},
1499                                         a = [];
1500
1501                                 for ( key in o ) {
1502                                         a.push( key );
1503                                 }
1504                                 a.sort();
1505                                 for ( key = 0; key < a.length; key++ ) {
1506                                         sorted[ a[ key ] ] = o[ a[ key ] ];
1507                                 }
1508                                 return sorted;
1509                         }
1510
1511                         /**
1512                          * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1513                          * to a query string of the form foo.bar,baz|bar.baz,quux
1514                          *
1515                          * @private
1516                          * @param {Object} moduleMap Module map
1517                          * @return {string} Module query string
1518                          */
1519                         function buildModulesString( moduleMap ) {
1520                                 var p, prefix,
1521                                         arr = [];
1522
1523                                 for ( prefix in moduleMap ) {
1524                                         p = prefix === '' ? '' : prefix + '.';
1525                                         arr.push( p + moduleMap[ prefix ].join( ',' ) );
1526                                 }
1527                                 return arr.join( '|' );
1528                         }
1529
1530                         /**
1531                          * Make a network request to load modules from the server.
1532                          *
1533                          * @private
1534                          * @param {Object} moduleMap Module map, see #buildModulesString
1535                          * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1536                          * @param {string} sourceLoadScript URL of load.php
1537                          */
1538                         function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
1539                                 // Optimisation: Inherit (Object.create), not copy ($.extend)
1540                                 var query = Object.create( currReqBase );
1541                                 query.modules = buildModulesString( moduleMap );
1542                                 query = sortQuery( query );
1543                                 addScript( sourceLoadScript + '?' + $.param( query ) );
1544                         }
1545
1546                         /**
1547                          * Resolve indexed dependencies.
1548                          *
1549                          * ResourceLoader uses an optimization to save space which replaces module names in
1550                          * dependency lists with the index of that module within the array of module
1551                          * registration data if it exists. The benefit is a significant reduction in the data
1552                          * size of the startup module. This function changes those dependency lists back to
1553                          * arrays of strings.
1554                          *
1555                          * @private
1556                          * @param {Array} modules Modules array
1557                          */
1558                         function resolveIndexedDependencies( modules ) {
1559                                 var i, j, deps;
1560                                 function resolveIndex( dep ) {
1561                                         return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1562                                 }
1563                                 for ( i = 0; i < modules.length; i++ ) {
1564                                         deps = modules[ i ][ 2 ];
1565                                         if ( deps ) {
1566                                                 for ( j = 0; j < deps.length; j++ ) {
1567                                                         deps[ j ] = resolveIndex( deps[ j ] );
1568                                                 }
1569                                         }
1570                                 }
1571                         }
1572
1573                         /**
1574                          * Create network requests for a batch of modules.
1575                          *
1576                          * This is an internal method for #work(). This must not be called directly
1577                          * unless the modules are already registered, and no request is in progress,
1578                          * and the module state has already been set to `loading`.
1579                          *
1580                          * @private
1581                          * @param {string[]} batch
1582                          */
1583                         function batchRequest( batch ) {
1584                                 var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
1585                                         source, group, i, modules, sourceLoadScript,
1586                                         currReqBase, currReqBaseLength, moduleMap, l,
1587                                         lastDotIndex, prefix, suffix, bytesAdded;
1588
1589                                 if ( !batch.length ) {
1590                                         return;
1591                                 }
1592
1593                                 // Always order modules alphabetically to help reduce cache
1594                                 // misses for otherwise identical content.
1595                                 batch.sort();
1596
1597                                 // Build a list of query parameters common to all requests
1598                                 reqBase = {
1599                                         skin: mw.config.get( 'skin' ),
1600                                         lang: mw.config.get( 'wgUserLanguage' ),
1601                                         debug: mw.config.get( 'debug' )
1602                                 };
1603                                 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
1604
1605                                 // Split module list by source and by group.
1606                                 splits = {};
1607                                 for ( b = 0; b < batch.length; b++ ) {
1608                                         bSource = registry[ batch[ b ] ].source;
1609                                         bGroup = registry[ batch[ b ] ].group;
1610                                         if ( !hasOwn.call( splits, bSource ) ) {
1611                                                 splits[ bSource ] = {};
1612                                         }
1613                                         if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
1614                                                 splits[ bSource ][ bGroup ] = [];
1615                                         }
1616                                         bSourceGroup = splits[ bSource ][ bGroup ];
1617                                         bSourceGroup.push( batch[ b ] );
1618                                 }
1619
1620                                 for ( source in splits ) {
1621
1622                                         sourceLoadScript = sources[ source ];
1623
1624                                         for ( group in splits[ source ] ) {
1625
1626                                                 // Cache access to currently selected list of
1627                                                 // modules for this group from this source.
1628                                                 modules = splits[ source ][ group ];
1629
1630                                                 // Optimisation: Inherit (Object.create), not copy ($.extend)
1631                                                 currReqBase = Object.create( reqBase );
1632                                                 currReqBase.version = getCombinedVersion( modules );
1633
1634                                                 // For user modules append a user name to the query string.
1635                                                 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1636                                                         currReqBase.user = mw.config.get( 'wgUserName' );
1637                                                 }
1638                                                 currReqBaseLength = $.param( currReqBase ).length;
1639                                                 // We may need to split up the request to honor the query string length limit,
1640                                                 // so build it piece by piece.
1641                                                 l = currReqBaseLength + 9; // '&modules='.length == 9
1642
1643                                                 moduleMap = {}; // { prefix: [ suffixes ] }
1644
1645                                                 for ( i = 0; i < modules.length; i++ ) {
1646                                                         // Determine how many bytes this module would add to the query string
1647                                                         lastDotIndex = modules[ i ].lastIndexOf( '.' );
1648
1649                                                         // If lastDotIndex is -1, substr() returns an empty string
1650                                                         prefix = modules[ i ].substr( 0, lastDotIndex );
1651                                                         suffix = modules[ i ].slice( lastDotIndex + 1 );
1652
1653                                                         bytesAdded = hasOwn.call( moduleMap, prefix ) ?
1654                                                                 suffix.length + 3 : // '%2C'.length == 3
1655                                                                 modules[ i ].length + 3; // '%7C'.length == 3
1656
1657                                                         // If the url would become too long, create a new one,
1658                                                         // but don't create empty requests
1659                                                         if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1660                                                                 // This url would become too long, create a new one, and start the old one
1661                                                                 doRequest( moduleMap, currReqBase, sourceLoadScript );
1662                                                                 moduleMap = {};
1663                                                                 l = currReqBaseLength + 9;
1664                                                                 mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
1665                                                         }
1666                                                         if ( !hasOwn.call( moduleMap, prefix ) ) {
1667                                                                 moduleMap[ prefix ] = [];
1668                                                         }
1669                                                         moduleMap[ prefix ].push( suffix );
1670                                                         l += bytesAdded;
1671                                                 }
1672                                                 // If there's anything left in moduleMap, request that too
1673                                                 if ( !$.isEmptyObject( moduleMap ) ) {
1674                                                         doRequest( moduleMap, currReqBase, sourceLoadScript );
1675                                                 }
1676                                         }
1677                                 }
1678                         }
1679
1680                         /**
1681                          * @private
1682                          * @param {string[]} implementations Array containing pieces of JavaScript code in the
1683                          *  form of calls to mw.loader#implement().
1684                          * @param {Function} cb Callback in case of failure
1685                          * @param {Error} cb.err
1686                          */
1687                         function asyncEval( implementations, cb ) {
1688                                 if ( !implementations.length ) {
1689                                         return;
1690                                 }
1691                                 mw.requestIdleCallback( function () {
1692                                         try {
1693                                                 $.globalEval( implementations.join( ';' ) );
1694                                         } catch ( err ) {
1695                                                 cb( err );
1696                                         }
1697                                 } );
1698                         }
1699
1700                         /**
1701                          * Make a versioned key for a specific module.
1702                          *
1703                          * @private
1704                          * @param {string} module Module name
1705                          * @return {string|null} Module key in format '`[name]@[version]`',
1706                          *  or null if the module does not exist
1707                          */
1708                         function getModuleKey( module ) {
1709                                 return hasOwn.call( registry, module ) ?
1710                                         ( module + '@' + registry[ module ].version ) : null;
1711                         }
1712
1713                         /**
1714                          * @private
1715                          * @param {string} key Module name or '`[name]@[version]`'
1716                          * @return {Object}
1717                          */
1718                         function splitModuleKey( key ) {
1719                                 var index = key.indexOf( '@' );
1720                                 if ( index === -1 ) {
1721                                         return { name: key };
1722                                 }
1723                                 return {
1724                                         name: key.slice( 0, index ),
1725                                         version: key.slice( index + 1 )
1726                                 };
1727                         }
1728
1729                         /* Public Members */
1730                         return {
1731                                 /**
1732                                  * The module registry is exposed as an aid for debugging and inspecting page
1733                                  * state; it is not a public interface for modifying the registry.
1734                                  *
1735                                  * @see #registry
1736                                  * @property
1737                                  * @private
1738                                  */
1739                                 moduleRegistry: registry,
1740
1741                                 /**
1742                                  * @inheritdoc #newStyleTag
1743                                  * @method
1744                                  */
1745                                 addStyleTag: newStyleTag,
1746
1747                                 /**
1748                                  * Start loading of all queued module dependencies.
1749                                  *
1750                                  * @protected
1751                                  */
1752                                 work: function () {
1753                                         var q, batch, implementations, sourceModules;
1754
1755                                         batch = [];
1756
1757                                         // Appends a list of modules from the queue to the batch
1758                                         for ( q = 0; q < queue.length; q++ ) {
1759                                                 // Only load modules which are registered
1760                                                 if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
1761                                                         // Prevent duplicate entries
1762                                                         if ( $.inArray( queue[ q ], batch ) === -1 ) {
1763                                                                 batch.push( queue[ q ] );
1764                                                                 // Mark registered modules as loading
1765                                                                 registry[ queue[ q ] ].state = 'loading';
1766                                                         }
1767                                                 }
1768                                         }
1769
1770                                         // Now that the queue has been processed into a batch, clear the queue.
1771                                         // This MUST happen before we initiate any eval or network request. Otherwise,
1772                                         // it is possible for a cached script to instantly trigger the same work queue
1773                                         // again; all before we've cleared it causing each request to include modules
1774                                         // which are already loaded.
1775                                         queue = [];
1776
1777                                         if ( !batch.length ) {
1778                                                 return;
1779                                         }
1780
1781                                         mw.loader.store.init();
1782                                         if ( mw.loader.store.enabled ) {
1783                                                 implementations = [];
1784                                                 sourceModules = [];
1785                                                 batch = $.grep( batch, function ( module ) {
1786                                                         var implementation = mw.loader.store.get( module );
1787                                                         if ( implementation ) {
1788                                                                 implementations.push( implementation );
1789                                                                 sourceModules.push( module );
1790                                                                 return false;
1791                                                         }
1792                                                         return true;
1793                                                 } );
1794                                                 asyncEval( implementations, function ( err ) {
1795                                                         var failed;
1796                                                         // Not good, the cached mw.loader.implement calls failed! This should
1797                                                         // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1798                                                         // Depending on how corrupt the string is, it is likely that some
1799                                                         // modules' implement() succeeded while the ones after the error will
1800                                                         // never run and leave their modules in the 'loading' state forever.
1801                                                         mw.loader.store.stats.failed++;
1802
1803                                                         // Since this is an error not caused by an individual module but by
1804                                                         // something that infected the implement call itself, don't take any
1805                                                         // risks and clear everything in this cache.
1806                                                         mw.loader.store.clear();
1807
1808                                                         mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
1809                                                         // Re-add the failed ones that are still pending back to the batch
1810                                                         failed = $.grep( sourceModules, function ( module ) {
1811                                                                 return registry[ module ].state === 'loading';
1812                                                         } );
1813                                                         batchRequest( failed );
1814                                                 } );
1815                                         }
1816
1817                                         batchRequest( batch );
1818                                 },
1819
1820                                 /**
1821                                  * Register a source.
1822                                  *
1823                                  * The #work() method will use this information to split up requests by source.
1824                                  *
1825                                  *     mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1826                                  *
1827                                  * @param {string|Object} id Source ID, or object mapping ids to load urls
1828                                  * @param {string} loadUrl Url to a load.php end point
1829                                  * @throws {Error} If source id is already registered
1830                                  */
1831                                 addSource: function ( id, loadUrl ) {
1832                                         var source;
1833                                         // Allow multiple additions
1834                                         if ( typeof id === 'object' ) {
1835                                                 for ( source in id ) {
1836                                                         mw.loader.addSource( source, id[ source ] );
1837                                                 }
1838                                                 return;
1839                                         }
1840
1841                                         if ( hasOwn.call( sources, id ) ) {
1842                                                 throw new Error( 'source already registered: ' + id );
1843                                         }
1844
1845                                         sources[ id ] = loadUrl;
1846                                 },
1847
1848                                 /**
1849                                  * Register a module, letting the system know about it and its properties.
1850                                  *
1851                                  * The startup modules contain calls to this method.
1852                                  *
1853                                  * When using multiple module registration by passing an array, dependencies that
1854                                  * are specified as references to modules within the array will be resolved before
1855                                  * the modules are registered.
1856                                  *
1857                                  * @param {string|Array} module Module name or array of arrays, each containing
1858                                  *  a list of arguments compatible with this method
1859                                  * @param {string|number} version Module version hash (falls backs to empty string)
1860                                  *  Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
1861                                  * @param {string|Array|Function} dependencies One string or array of strings of module
1862                                  *  names on which this module depends, or a function that returns that array.
1863                                  * @param {string} [group=null] Group which the module is in
1864                                  * @param {string} [source='local'] Name of the source
1865                                  * @param {string} [skip=null] Script body of the skip function
1866                                  */
1867                                 register: function ( module, version, dependencies, group, source, skip ) {
1868                                         var i, deps;
1869                                         // Allow multiple registration
1870                                         if ( typeof module === 'object' ) {
1871                                                 resolveIndexedDependencies( module );
1872                                                 for ( i = 0; i < module.length; i++ ) {
1873                                                         // module is an array of module names
1874                                                         if ( typeof module[ i ] === 'string' ) {
1875                                                                 mw.loader.register( module[ i ] );
1876                                                         // module is an array of arrays
1877                                                         } else if ( typeof module[ i ] === 'object' ) {
1878                                                                 mw.loader.register.apply( mw.loader, module[ i ] );
1879                                                         }
1880                                                 }
1881                                                 return;
1882                                         }
1883                                         if ( hasOwn.call( registry, module ) ) {
1884                                                 throw new Error( 'module already registered: ' + module );
1885                                         }
1886                                         if ( typeof dependencies === 'string' ) {
1887                                                 // A single module name
1888                                                 deps = [ dependencies ];
1889                                         } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
1890                                                 // Array of module names or a function that returns an array
1891                                                 deps = dependencies;
1892                                         }
1893                                         // List the module as registered
1894                                         registry[ module ] = {
1895                                                 // Exposed to execute() for mw.loader.implement() closures.
1896                                                 // Import happens via require().
1897                                                 module: {
1898                                                         exports: {}
1899                                                 },
1900                                                 version: version !== undefined ? String( version ) : '',
1901                                                 dependencies: deps || [],
1902                                                 group: typeof group === 'string' ? group : null,
1903                                                 source: typeof source === 'string' ? source : 'local',
1904                                                 state: 'registered',
1905                                                 skip: typeof skip === 'string' ? skip : null
1906                                         };
1907                                 },
1908
1909                                 /**
1910                                  * Implement a module given the components that make up the module.
1911                                  *
1912                                  * When #load() or #using() requests one or more modules, the server
1913                                  * response contain calls to this function.
1914                                  *
1915                                  * @param {string} module Name of module and current module version. Formatted
1916                                  *  as '`[name]@[version]`". This version should match the requested version
1917                                  *  (from #batchRequest and #registry). This avoids race conditions (T117587).
1918                                  *  For back-compat with MediaWiki 1.27 and earlier, the version may be omitted.
1919                                  * @param {Function|Array|string} [script] Function with module code, list of URLs
1920                                  *  to load via `<script src>`, or string of module code for `$.globalEval()`.
1921                                  * @param {Object} [style] Should follow one of the following patterns:
1922                                  *
1923                                  *     { "css": [css, ..] }
1924                                  *     { "url": { <media>: [url, ..] } }
1925                                  *
1926                                  * And for backwards compatibility (needs to be supported forever due to caching):
1927                                  *
1928                                  *     { <media>: css }
1929                                  *     { <media>: [url, ..] }
1930                                  *
1931                                  * The reason css strings are not concatenated anymore is T33676. We now check
1932                                  * whether it's safe to extend the stylesheet.
1933                                  *
1934                                  * @protected
1935                                  * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1936                                  * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1937                                  */
1938                                 implement: function ( module, script, style, messages, templates ) {
1939                                         var split = splitModuleKey( module ),
1940                                                 name = split.name,
1941                                                 version = split.version;
1942                                         // Automatically register module
1943                                         if ( !hasOwn.call( registry, name ) ) {
1944                                                 mw.loader.register( name );
1945                                         }
1946                                         // Check for duplicate implementation
1947                                         if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
1948                                                 throw new Error( 'module already implemented: ' + name );
1949                                         }
1950                                         if ( version ) {
1951                                                 // Without this reset, if there is a version mismatch between the
1952                                                 // requested and received module version, then mw.loader.store would
1953                                                 // cache the response under the requested key. Thus poisoning the cache
1954                                                 // indefinitely with a stale value. (T117587)
1955                                                 registry[ name ].version = version;
1956                                         }
1957                                         // Attach components
1958                                         registry[ name ].script = script || null;
1959                                         registry[ name ].style = style || null;
1960                                         registry[ name ].messages = messages || null;
1961                                         registry[ name ].templates = templates || null;
1962                                         // The module may already have been marked as erroneous
1963                                         if ( $.inArray( registry[ name ].state, [ 'error', 'missing' ] ) === -1 ) {
1964                                                 registry[ name ].state = 'loaded';
1965                                                 if ( allReady( registry[ name ].dependencies ) ) {
1966                                                         execute( name );
1967                                                 }
1968                                         }
1969                                 },
1970
1971                                 /**
1972                                  * Execute a function as soon as one or more required modules are ready.
1973                                  *
1974                                  * Example of inline dependency on OOjs:
1975                                  *
1976                                  *     mw.loader.using( 'oojs', function () {
1977                                  *         OO.compare( [ 1 ], [ 1 ] );
1978                                  *     } );
1979                                  *
1980                                  * Since MediaWiki 1.23 this also returns a promise.
1981                                  *
1982                                  * Since MediaWiki 1.28 the promise is resolved with a `require` function.
1983                                  *
1984                                  * @param {string|Array} dependencies Module name or array of modules names the
1985                                  *  callback depends on to be ready before executing
1986                                  * @param {Function} [ready] Callback to execute when all dependencies are ready
1987                                  * @param {Function} [error] Callback to execute if one or more dependencies failed
1988                                  * @return {jQuery.Promise} With a `require` function
1989                                  */
1990                                 using: function ( dependencies, ready, error ) {
1991                                         var deferred = $.Deferred();
1992
1993                                         // Allow calling with a single dependency as a string
1994                                         if ( typeof dependencies === 'string' ) {
1995                                                 dependencies = [ dependencies ];
1996                                         }
1997
1998                                         if ( ready ) {
1999                                                 deferred.done( ready );
2000                                         }
2001                                         if ( error ) {
2002                                                 deferred.fail( error );
2003                                         }
2004
2005                                         try {
2006                                                 // Resolve entire dependency map
2007                                                 dependencies = resolve( dependencies );
2008                                         } catch ( e ) {
2009                                                 return deferred.reject( e ).promise();
2010                                         }
2011                                         if ( allReady( dependencies ) ) {
2012                                                 // Run ready immediately
2013                                                 deferred.resolve( mw.loader.require );
2014                                         } else if ( anyFailed( dependencies ) ) {
2015                                                 // Execute error immediately if any dependencies have errors
2016                                                 deferred.reject(
2017                                                         new Error( 'One or more dependencies failed to load' ),
2018                                                         dependencies
2019                                                 );
2020                                         } else {
2021                                                 // Not all dependencies are ready, add to the load queue
2022                                                 enqueue( dependencies, function () {
2023                                                         deferred.resolve( mw.loader.require );
2024                                                 }, deferred.reject );
2025                                         }
2026
2027                                         return deferred.promise();
2028                                 },
2029
2030                                 /**
2031                                  * Load an external script or one or more modules.
2032                                  *
2033                                  * This method takes a list of unrelated modules. Use cases:
2034                                  *
2035                                  * - A web page will be composed of many different widgets. These widgets independently
2036                                  *   queue their ResourceLoader modules (`OutputPage::addModules()`). If any of them
2037                                  *   have problems, or are no longer known (e.g. cached HTML), the other modules
2038                                  *   should still be loaded.
2039                                  * - This method is used for preloading, which must not throw. Later code that
2040                                  *   calls #using() will handle the error.
2041                                  *
2042                                  * @param {string|Array} modules Either the name of a module, array of modules,
2043                                  *  or a URL of an external script or style
2044                                  * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
2045                                  *  external script or style; acceptable values are "text/css" and
2046                                  *  "text/javascript"; if no type is provided, text/javascript is assumed.
2047                                  */
2048                                 load: function ( modules, type ) {
2049                                         var filtered, l;
2050
2051                                         // Allow calling with a url or single dependency as a string
2052                                         if ( typeof modules === 'string' ) {
2053                                                 // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
2054                                                 if ( /^(https?:)?\/?\//.test( modules ) ) {
2055                                                         if ( type === 'text/css' ) {
2056                                                                 l = document.createElement( 'link' );
2057                                                                 l.rel = 'stylesheet';
2058                                                                 l.href = modules;
2059                                                                 $( 'head' ).append( l );
2060                                                                 return;
2061                                                         }
2062                                                         if ( type === 'text/javascript' || type === undefined ) {
2063                                                                 addScript( modules );
2064                                                                 return;
2065                                                         }
2066                                                         // Unknown type
2067                                                         throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
2068                                                 }
2069                                                 // Called with single module
2070                                                 modules = [ modules ];
2071                                         }
2072
2073                                         // Filter out top-level modules that are unknown or failed to load before.
2074                                         filtered = $.grep( modules, function ( module ) {
2075                                                 var state = mw.loader.getState( module );
2076                                                 return state !== 'error' && state !== 'missing';
2077                                         } );
2078                                         // Resolve remaining list using the known dependency tree.
2079                                         // This also filters out modules with unknown dependencies. (T36853)
2080                                         filtered = resolveStubbornly( filtered );
2081                                         // If all modules are ready, or if any modules have errors, nothing to be done.
2082                                         if ( allReady( filtered ) || anyFailed( filtered ) ) {
2083                                                 return;
2084                                         }
2085                                         if ( filtered.length === 0 ) {
2086                                                 return;
2087                                         }
2088                                         // Some modules are not yet ready, add to module load queue.
2089                                         enqueue( filtered, undefined, undefined );
2090                                 },
2091
2092                                 /**
2093                                  * Change the state of one or more modules.
2094                                  *
2095                                  * @param {string|Object} module Module name or object of module name/state pairs
2096                                  * @param {string} state State name
2097                                  */
2098                                 state: function ( module, state ) {
2099                                         var m;
2100
2101                                         if ( typeof module === 'object' ) {
2102                                                 for ( m in module ) {
2103                                                         mw.loader.state( m, module[ m ] );
2104                                                 }
2105                                                 return;
2106                                         }
2107                                         if ( !hasOwn.call( registry, module ) ) {
2108                                                 mw.loader.register( module );
2109                                         }
2110                                         registry[ module ].state = state;
2111                                         if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1 ) {
2112                                                 // Make sure pending modules depending on this one get executed if their
2113                                                 // dependencies are now fulfilled!
2114                                                 handlePending( module );
2115                                         }
2116                                 },
2117
2118                                 /**
2119                                  * Get the version of a module.
2120                                  *
2121                                  * @param {string} module Name of module
2122                                  * @return {string|null} The version, or null if the module (or its version) is not
2123                                  *  in the registry.
2124                                  */
2125                                 getVersion: function ( module ) {
2126                                         if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) {
2127                                                 return null;
2128                                         }
2129                                         return registry[ module ].version;
2130                                 },
2131
2132                                 /**
2133                                  * Get the state of a module.
2134                                  *
2135                                  * @param {string} module Name of module
2136                                  * @return {string|null} The state, or null if the module (or its state) is not
2137                                  *  in the registry.
2138                                  */
2139                                 getState: function ( module ) {
2140                                         if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) {
2141                                                 return null;
2142                                         }
2143                                         return registry[ module ].state;
2144                                 },
2145
2146                                 /**
2147                                  * Get the names of all registered modules.
2148                                  *
2149                                  * @return {Array}
2150                                  */
2151                                 getModuleNames: function () {
2152                                         return Object.keys( registry );
2153                                 },
2154
2155                                 /**
2156                                  * Get the exported value of a module.
2157                                  *
2158                                  * Modules may provide this via their local `module.exports`.
2159                                  *
2160                                  * @protected
2161                                  * @since 1.27
2162                                  * @param {string} moduleName Module name
2163                                  * @return {Mixed} Exported value
2164                                  */
2165                                 require: function ( moduleName ) {
2166                                         var state = mw.loader.getState( moduleName );
2167
2168                                         // Only ready modules can be required
2169                                         if ( state !== 'ready' ) {
2170                                                 // Module may've forgotten to declare a dependency
2171                                                 throw new Error( 'Module "' + moduleName + '" is not loaded.' );
2172                                         }
2173
2174                                         return registry[ moduleName ].module.exports;
2175                                 },
2176
2177                                 /**
2178                                  * @inheritdoc mw.inspect#runReports
2179                                  * @method
2180                                  */
2181                                 inspect: function () {
2182                                         var args = slice.call( arguments );
2183                                         mw.loader.using( 'mediawiki.inspect', function () {
2184                                                 mw.inspect.runReports.apply( mw.inspect, args );
2185                                         } );
2186                                 },
2187
2188                                 /**
2189                                  * On browsers that implement the localStorage API, the module store serves as a
2190                                  * smart complement to the browser cache. Unlike the browser cache, the module store
2191                                  * can slice a concatenated response from ResourceLoader into its constituent
2192                                  * modules and cache each of them separately, using each module's versioning scheme
2193                                  * to determine when the cache should be invalidated.
2194                                  *
2195                                  * @singleton
2196                                  * @class mw.loader.store
2197                                  */
2198                                 store: {
2199                                         // Whether the store is in use on this page.
2200                                         enabled: null,
2201
2202                                         // Modules whose string representation exceeds 100 kB are
2203                                         // ineligible for storage. See bug T66721.
2204                                         MODULE_SIZE_MAX: 100 * 1000,
2205
2206                                         // The contents of the store, mapping '[name]@[version]' keys
2207                                         // to module implementations.
2208                                         items: {},
2209
2210                                         // Cache hit stats
2211                                         stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
2212
2213                                         /**
2214                                          * Construct a JSON-serializable object representing the content of the store.
2215                                          *
2216                                          * @return {Object} Module store contents.
2217                                          */
2218                                         toJSON: function () {
2219                                                 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2220                                         },
2221
2222                                         /**
2223                                          * Get the localStorage key for the entire module store. The key references
2224                                          * $wgDBname to prevent clashes between wikis which share a common host.
2225                                          *
2226                                          * @return {string} localStorage item key
2227                                          */
2228                                         getStoreKey: function () {
2229                                                 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2230                                         },
2231
2232                                         /**
2233                                          * Get a key on which to vary the module cache.
2234                                          *
2235                                          * @return {string} String of concatenated vary conditions.
2236                                          */
2237                                         getVary: function () {
2238                                                 return [
2239                                                         mw.config.get( 'skin' ),
2240                                                         mw.config.get( 'wgResourceLoaderStorageVersion' ),
2241                                                         mw.config.get( 'wgUserLanguage' )
2242                                                 ].join( ':' );
2243                                         },
2244
2245                                         /**
2246                                          * Initialize the store.
2247                                          *
2248                                          * Retrieves store from localStorage and (if successfully retrieved) decoding
2249                                          * the stored JSON value to a plain object.
2250                                          *
2251                                          * The try / catch block is used for JSON & localStorage feature detection.
2252                                          * See the in-line documentation for Modernizr's localStorage feature detection
2253                                          * code for a full account of why we need a try / catch:
2254                                          * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2255                                          */
2256                                         init: function () {
2257                                                 var raw, data;
2258
2259                                                 if ( mw.loader.store.enabled !== null ) {
2260                                                         // Init already ran
2261                                                         return;
2262                                                 }
2263
2264                                                 if (
2265                                                         // Disabled because localStorage quotas are tight and (in Firefox's case)
2266                                                         // shared by multiple origins.
2267                                                         // See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
2268                                                         /Firefox|Opera/.test( navigator.userAgent ) ||
2269
2270                                                         // Disabled by configuration.
2271                                                         !mw.config.get( 'wgResourceLoaderStorageEnabled' )
2272                                                 ) {
2273                                                         // Clear any previous store to free up space. (T66721)
2274                                                         mw.loader.store.clear();
2275                                                         mw.loader.store.enabled = false;
2276                                                         return;
2277                                                 }
2278                                                 if ( mw.config.get( 'debug' ) ) {
2279                                                         // Disable module store in debug mode
2280                                                         mw.loader.store.enabled = false;
2281                                                         return;
2282                                                 }
2283
2284                                                 try {
2285                                                         raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2286                                                         // If we get here, localStorage is available; mark enabled
2287                                                         mw.loader.store.enabled = true;
2288                                                         data = JSON.parse( raw );
2289                                                         if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2290                                                                 mw.loader.store.items = data.items;
2291                                                                 return;
2292                                                         }
2293                                                 } catch ( e ) {
2294                                                         mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
2295                                                 }
2296
2297                                                 if ( raw === undefined ) {
2298                                                         // localStorage failed; disable store
2299                                                         mw.loader.store.enabled = false;
2300                                                 } else {
2301                                                         mw.loader.store.update();
2302                                                 }
2303                                         },
2304
2305                                         /**
2306                                          * Retrieve a module from the store and update cache hit stats.
2307                                          *
2308                                          * @param {string} module Module name
2309                                          * @return {string|boolean} Module implementation or false if unavailable
2310                                          */
2311                                         get: function ( module ) {
2312                                                 var key;
2313
2314                                                 if ( !mw.loader.store.enabled ) {
2315                                                         return false;
2316                                                 }
2317
2318                                                 key = getModuleKey( module );
2319                                                 if ( key in mw.loader.store.items ) {
2320                                                         mw.loader.store.stats.hits++;
2321                                                         return mw.loader.store.items[ key ];
2322                                                 }
2323                                                 mw.loader.store.stats.misses++;
2324                                                 return false;
2325                                         },
2326
2327                                         /**
2328                                          * Stringify a module and queue it for storage.
2329                                          *
2330                                          * @param {string} module Module name
2331                                          * @param {Object} descriptor The module's descriptor as set in the registry
2332                                          * @return {boolean} Module was set
2333                                          */
2334                                         set: function ( module, descriptor ) {
2335                                                 var args, key, src;
2336
2337                                                 if ( !mw.loader.store.enabled ) {
2338                                                         return false;
2339                                                 }
2340
2341                                                 key = getModuleKey( module );
2342
2343                                                 if (
2344                                                         // Already stored a copy of this exact version
2345                                                         key in mw.loader.store.items ||
2346                                                         // Module failed to load
2347                                                         descriptor.state !== 'ready' ||
2348                                                         // Unversioned, private, or site-/user-specific
2349                                                         ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
2350                                                         // Partial descriptor
2351                                                         // (e.g. skipped module, or style module with state=ready)
2352                                                         $.inArray( undefined, [ descriptor.script, descriptor.style,
2353                                                                 descriptor.messages, descriptor.templates ] ) !== -1
2354                                                 ) {
2355                                                         // Decline to store
2356                                                         return false;
2357                                                 }
2358
2359                                                 try {
2360                                                         args = [
2361                                                                 JSON.stringify( key ),
2362                                                                 typeof descriptor.script === 'function' ?
2363                                                                         String( descriptor.script ) :
2364                                                                         JSON.stringify( descriptor.script ),
2365                                                                 JSON.stringify( descriptor.style ),
2366                                                                 JSON.stringify( descriptor.messages ),
2367                                                                 JSON.stringify( descriptor.templates )
2368                                                         ];
2369                                                         // Attempted workaround for a possible Opera bug (bug T59567).
2370                                                         // This regex should never match under sane conditions.
2371                                                         if ( /^\s*\(/.test( args[ 1 ] ) ) {
2372                                                                 args[ 1 ] = 'function' + args[ 1 ];
2373                                                                 mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
2374                                                         }
2375                                                 } catch ( e ) {
2376                                                         mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
2377                                                         return false;
2378                                                 }
2379
2380                                                 src = 'mw.loader.implement(' + args.join( ',' ) + ');';
2381                                                 if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
2382                                                         return false;
2383                                                 }
2384                                                 mw.loader.store.items[ key ] = src;
2385                                                 mw.loader.store.update();
2386                                                 return true;
2387                                         },
2388
2389                                         /**
2390                                          * Iterate through the module store, removing any item that does not correspond
2391                                          * (in name and version) to an item in the module registry.
2392                                          *
2393                                          * @return {boolean} Store was pruned
2394                                          */
2395                                         prune: function () {
2396                                                 var key, module;
2397
2398                                                 if ( !mw.loader.store.enabled ) {
2399                                                         return false;
2400                                                 }
2401
2402                                                 for ( key in mw.loader.store.items ) {
2403                                                         module = key.slice( 0, key.indexOf( '@' ) );
2404                                                         if ( getModuleKey( module ) !== key ) {
2405                                                                 mw.loader.store.stats.expired++;
2406                                                                 delete mw.loader.store.items[ key ];
2407                                                         } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
2408                                                                 // This value predates the enforcement of a size limit on cached modules.
2409                                                                 delete mw.loader.store.items[ key ];
2410                                                         }
2411                                                 }
2412                                                 return true;
2413                                         },
2414
2415                                         /**
2416                                          * Clear the entire module store right now.
2417                                          */
2418                                         clear: function () {
2419                                                 mw.loader.store.items = {};
2420                                                 try {
2421                                                         localStorage.removeItem( mw.loader.store.getStoreKey() );
2422                                                 } catch ( ignored ) {}
2423                                         },
2424
2425                                         /**
2426                                          * Sync in-memory store back to localStorage.
2427                                          *
2428                                          * This function debounces updates. When called with a flush already pending,
2429                                          * the call is coalesced into the pending update. The call to
2430                                          * localStorage.setItem will be naturally deferred until the page is quiescent.
2431                                          *
2432                                          * Because localStorage is shared by all pages from the same origin, if multiple
2433                                          * pages are loaded with different module sets, the possibility exists that
2434                                          * modules saved by one page will be clobbered by another. But the impact would
2435                                          * be minor and the problem would be corrected by subsequent page views.
2436                                          *
2437                                          * @method
2438                                          */
2439                                         update: ( function () {
2440                                                 var hasPendingWrite = false;
2441
2442                                                 function flushWrites() {
2443                                                         var data, key;
2444                                                         if ( !hasPendingWrite || !mw.loader.store.enabled ) {
2445                                                                 return;
2446                                                         }
2447
2448                                                         mw.loader.store.prune();
2449                                                         key = mw.loader.store.getStoreKey();
2450                                                         try {
2451                                                                 // Replacing the content of the module store might fail if the new
2452                                                                 // contents would exceed the browser's localStorage size limit. To
2453                                                                 // avoid clogging the browser with stale data, always remove the old
2454                                                                 // value before attempting to set the new one.
2455                                                                 localStorage.removeItem( key );
2456                                                                 data = JSON.stringify( mw.loader.store );
2457                                                                 localStorage.setItem( key, data );
2458                                                         } catch ( e ) {
2459                                                                 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
2460                                                         }
2461
2462                                                         hasPendingWrite = false;
2463                                                 }
2464
2465                                                 return function () {
2466                                                         if ( !hasPendingWrite ) {
2467                                                                 hasPendingWrite = true;
2468                                                                 mw.requestIdleCallback( flushWrites );
2469                                                         }
2470                                                 };
2471                                         }() )
2472                                 }
2473                         };
2474                 }() ),
2475
2476                 /**
2477                  * HTML construction helper functions
2478                  *
2479                  *     @example
2480                  *
2481                  *     var Html, output;
2482                  *
2483                  *     Html = mw.html;
2484                  *     output = Html.element( 'div', {}, new Html.Raw(
2485                  *         Html.element( 'img', { src: '<' } )
2486                  *     ) );
2487                  *     mw.log( output ); // <div><img src="&lt;"/></div>
2488                  *
2489                  * @class mw.html
2490                  * @singleton
2491                  */
2492                 html: ( function () {
2493                         function escapeCallback( s ) {
2494                                 switch ( s ) {
2495                                         case '\'':
2496                                                 return '&#039;';
2497                                         case '"':
2498                                                 return '&quot;';
2499                                         case '<':
2500                                                 return '&lt;';
2501                                         case '>':
2502                                                 return '&gt;';
2503                                         case '&':
2504                                                 return '&amp;';
2505                                 }
2506                         }
2507
2508                         return {
2509                                 /**
2510                                  * Escape a string for HTML.
2511                                  *
2512                                  * Converts special characters to HTML entities.
2513                                  *
2514                                  *     mw.html.escape( '< > \' & "' );
2515                                  *     // Returns &lt; &gt; &#039; &amp; &quot;
2516                                  *
2517                                  * @param {string} s The string to escape
2518                                  * @return {string} HTML
2519                                  */
2520                                 escape: function ( s ) {
2521                                         return s.replace( /['"<>&]/g, escapeCallback );
2522                                 },
2523
2524                                 /**
2525                                  * Create an HTML element string, with safe escaping.
2526                                  *
2527                                  * @param {string} name The tag name.
2528                                  * @param {Object} [attrs] An object with members mapping element names to values
2529                                  * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
2530                                  *
2531                                  *  - string: Text to be escaped.
2532                                  *  - null: The element is treated as void with short closing form, e.g. `<br/>`.
2533                                  *  - this.Raw: The raw value is directly included.
2534                                  *  - this.Cdata: The raw value is directly included. An exception is
2535                                  *    thrown if it contains any illegal ETAGO delimiter.
2536                                  *    See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
2537                                  * @return {string} HTML
2538                                  */
2539                                 element: function ( name, attrs, contents ) {
2540                                         var v, attrName, s = '<' + name;
2541
2542                                         if ( attrs ) {
2543                                                 for ( attrName in attrs ) {
2544                                                         v = attrs[ attrName ];
2545                                                         // Convert name=true, to name=name
2546                                                         if ( v === true ) {
2547                                                                 v = attrName;
2548                                                         // Skip name=false
2549                                                         } else if ( v === false ) {
2550                                                                 continue;
2551                                                         }
2552                                                         s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2553                                                 }
2554                                         }
2555                                         if ( contents === undefined || contents === null ) {
2556                                                 // Self close tag
2557                                                 s += '/>';
2558                                                 return s;
2559                                         }
2560                                         // Regular open tag
2561                                         s += '>';
2562                                         switch ( typeof contents ) {
2563                                                 case 'string':
2564                                                         // Escaped
2565                                                         s += this.escape( contents );
2566                                                         break;
2567                                                 case 'number':
2568                                                 case 'boolean':
2569                                                         // Convert to string
2570                                                         s += String( contents );
2571                                                         break;
2572                                                 default:
2573                                                         if ( contents instanceof this.Raw ) {
2574                                                                 // Raw HTML inclusion
2575                                                                 s += contents.value;
2576                                                         } else if ( contents instanceof this.Cdata ) {
2577                                                                 // CDATA
2578                                                                 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2579                                                                         throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2580                                                                 }
2581                                                                 s += contents.value;
2582                                                         } else {
2583                                                                 throw new Error( 'mw.html.element: Invalid type of contents' );
2584                                                         }
2585                                         }
2586                                         s += '</' + name + '>';
2587                                         return s;
2588                                 },
2589
2590                                 /**
2591                                  * Wrapper object for raw HTML passed to mw.html.element().
2592                                  *
2593                                  * @class mw.html.Raw
2594                                  * @constructor
2595                                  * @param {string} value
2596                                  */
2597                                 Raw: function ( value ) {
2598                                         this.value = value;
2599                                 },
2600
2601                                 /**
2602                                  * Wrapper object for CDATA element contents passed to mw.html.element()
2603                                  *
2604                                  * @class mw.html.Cdata
2605                                  * @constructor
2606                                  * @param {string} value
2607                                  */
2608                                 Cdata: function ( value ) {
2609                                         this.value = value;
2610                                 }
2611                         };
2612                 }() ),
2613
2614                 // Skeleton user object, extended by the 'mediawiki.user' module.
2615                 /**
2616                  * @class mw.user
2617                  * @singleton
2618                  */
2619                 user: {
2620                         /**
2621                          * @property {mw.Map}
2622                          */
2623                         options: new Map(),
2624                         /**
2625                          * @property {mw.Map}
2626                          */
2627                         tokens: new Map()
2628                 },
2629
2630                 // OOUI widgets specific to MediaWiki
2631                 widgets: {},
2632
2633                 /**
2634                  * Registry and firing of events.
2635                  *
2636                  * MediaWiki has various interface components that are extended, enhanced
2637                  * or manipulated in some other way by extensions, gadgets and even
2638                  * in core itself.
2639                  *
2640                  * This framework helps streamlining the timing of when these other
2641                  * code paths fire their plugins (instead of using document-ready,
2642                  * which can and should be limited to firing only once).
2643                  *
2644                  * Features like navigating to other wiki pages, previewing an edit
2645                  * and editing itself – without a refresh – can then retrigger these
2646                  * hooks accordingly to ensure everything still works as expected.
2647                  *
2648                  * Example usage:
2649                  *
2650                  *     mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2651                  *     mw.hook( 'wikipage.content' ).fire( $content );
2652                  *
2653                  * Handlers can be added and fired for arbitrary event names at any time. The same
2654                  * event can be fired multiple times. The last run of an event is memorized
2655                  * (similar to `$(document).ready` and `$.Deferred().done`).
2656                  * This means if an event is fired, and a handler added afterwards, the added
2657                  * function will be fired right away with the last given event data.
2658                  *
2659                  * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2660                  * Thus allowing flexible use and optimal maintainability and authority control.
2661                  * You can pass around the `add` and/or `fire` method to another piece of code
2662                  * without it having to know the event name (or `mw.hook` for that matter).
2663                  *
2664                  *     var h = mw.hook( 'bar.ready' );
2665                  *     new mw.Foo( .. ).fetch( { callback: h.fire } );
2666                  *
2667                  * Note: Events are documented with an underscore instead of a dot in the event
2668                  * name due to jsduck not supporting dots in that position.
2669                  *
2670                  * @class mw.hook
2671                  */
2672                 hook: ( function () {
2673                         var lists = {};
2674
2675                         /**
2676                          * Create an instance of mw.hook.
2677                          *
2678                          * @method hook
2679                          * @member mw
2680                          * @param {string} name Name of hook.
2681                          * @return {mw.hook}
2682                          */
2683                         return function ( name ) {
2684                                 var list = hasOwn.call( lists, name ) ?
2685                                         lists[ name ] :
2686                                         lists[ name ] = $.Callbacks( 'memory' );
2687
2688                                 return {
2689                                         /**
2690                                          * Register a hook handler
2691                                          *
2692                                          * @param {...Function} handler Function to bind.
2693                                          * @chainable
2694                                          */
2695                                         add: list.add,
2696
2697                                         /**
2698                                          * Unregister a hook handler
2699                                          *
2700                                          * @param {...Function} handler Function to unbind.
2701                                          * @chainable
2702                                          */
2703                                         remove: list.remove,
2704
2705                                         // eslint-disable-next-line valid-jsdoc
2706                                         /**
2707                                          * Run a hook.
2708                                          *
2709                                          * @param {...Mixed} data
2710                                          * @chainable
2711                                          */
2712                                         fire: function () {
2713                                                 return list.fireWith.call( this, null, slice.call( arguments ) );
2714                                         }
2715                                 };
2716                         };
2717                 }() )
2718         };
2719
2720         // Alias $j to jQuery for backwards compatibility
2721         // @deprecated since 1.23 Use $ or jQuery instead
2722         mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2723
2724         /**
2725          * Log a message to window.console, if possible.
2726          *
2727          * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
2728          * also in production mode). Gets console references in each invocation instead of caching the
2729          * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
2730          *
2731          * @private
2732          * @param {string} topic Stream name passed by mw.track
2733          * @param {Object} data Data passed by mw.track
2734          * @param {Error} [data.exception]
2735          * @param {string} data.source Error source
2736          * @param {string} [data.module] Name of module which caused the error
2737          */
2738         function logError( topic, data ) {
2739                 /* eslint-disable no-console */
2740                 var msg,
2741                         e = data.exception,
2742                         source = data.source,
2743                         module = data.module,
2744                         console = window.console;
2745
2746                 if ( console && console.log ) {
2747                         msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
2748                         if ( module ) {
2749                                 msg += ' in module ' + module;
2750                         }
2751                         msg += ( e ? ':' : '.' );
2752                         console.log( msg );
2753
2754                         // If we have an exception object, log it to the warning channel to trigger
2755                         // proper stacktraces in browsers that support it.
2756                         if ( e && console.warn ) {
2757                                 console.warn( String( e ), e );
2758                         }
2759                 }
2760                 /* eslint-enable no-console */
2761         }
2762
2763         // Subscribe to error streams
2764         mw.trackSubscribe( 'resourceloader.exception', logError );
2765         mw.trackSubscribe( 'resourceloader.assert', logError );
2766
2767         /**
2768          * Fired when all modules associated with the page have finished loading.
2769          *
2770          * @event resourceloader_loadEnd
2771          * @member mw.hook
2772          */
2773         $( function () {
2774                 var loading, modules;
2775
2776                 modules = $.grep( mw.loader.getModuleNames(), function ( module ) {
2777                         return mw.loader.getState( module ) === 'loading';
2778                 } );
2779                 // We only need a callback, not any actual module. First try a single using()
2780                 // for all loading modules. If one fails, fall back to tracking each module
2781                 // separately via $.when(), this is expensive.
2782                 loading = mw.loader.using( modules ).then( null, function () {
2783                         var all = modules.map( function ( module ) {
2784                                 return mw.loader.using( module ).then( null, function () {
2785                                         return $.Deferred().resolve();
2786                                 } );
2787                         } );
2788                         return $.when.apply( $, all );
2789                 } );
2790                 loading.then( function () {
2791                         /* global mwPerformance */
2792                         mwPerformance.mark( 'mwLoadEnd' );
2793                         mw.hook( 'resourceloader.loadEnd' ).fire();
2794                 } );
2795         } );
2796
2797         // Attach to window and globally alias
2798         window.mw = window.mediaWiki = mw;
2799 }( jQuery ) );