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