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