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