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