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