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