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