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