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