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