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