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