Merge "Update OOjs UI to v0.1.0-pre (da4b0d5c14)"
[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 /**
487 * Templates associated with a module
488 * @property {mw.Map}
489 */
490 templates: new Map(),
491
492 /* Public Methods */
493
494 /**
495 * Get a message object.
496 *
497 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
498 *
499 * @see mw.Message
500 * @param {string} key Key of message to get
501 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
502 * @return {mw.Message}
503 */
504 message: function ( key ) {
505 // Variadic arguments
506 var parameters = slice.call( arguments, 1 );
507 return new Message( mw.messages, key, parameters );
508 },
509
510 /**
511 * Get a message string using the (default) 'text' format.
512 *
513 * Shortcut for `mw.message( key, parameters... ).text()`.
514 *
515 * @see mw.Message
516 * @param {string} key Key of message to get
517 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
518 * @return {string}
519 */
520 msg: function () {
521 return mw.message.apply( mw.message, arguments ).toString();
522 },
523
524 /**
525 * Dummy placeholder for {@link mw.log}
526 * @method
527 */
528 log: ( function () {
529 // Also update the restoration of methods in mediawiki.log.js
530 // when adding or removing methods here.
531 var log = function () {};
532
533 /**
534 * @class mw.log
535 * @singleton
536 */
537
538 /**
539 * Write a message the console's warning channel.
540 * Also logs a stacktrace for easier debugging.
541 * Each action is silently ignored if the browser doesn't support it.
542 *
543 * @param {string...} msg Messages to output to console
544 */
545 log.warn = function () {
546 var console = window.console;
547 if ( console && console.warn && console.warn.apply ) {
548 console.warn.apply( console, arguments );
549 if ( console.trace ) {
550 console.trace();
551 }
552 }
553 };
554
555 /**
556 * Create a property in a host object that, when accessed, will produce
557 * a deprecation warning in the console with backtrace.
558 *
559 * @param {Object} obj Host object of deprecated property
560 * @param {string} key Name of property to create in `obj`
561 * @param {Mixed} val The value this property should return when accessed
562 * @param {string} [msg] Optional text to include in the deprecation message.
563 */
564 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
565 obj[key] = val;
566 } : function ( obj, key, val, msg ) {
567 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
568 try {
569 Object.defineProperty( obj, key, {
570 configurable: true,
571 enumerable: true,
572 get: function () {
573 mw.track( 'mw.deprecate', key );
574 mw.log.warn( msg );
575 return val;
576 },
577 set: function ( newVal ) {
578 mw.track( 'mw.deprecate', key );
579 mw.log.warn( msg );
580 val = newVal;
581 }
582 } );
583 } catch ( err ) {
584 // IE8 can throw on Object.defineProperty
585 obj[key] = val;
586 }
587 };
588
589 return log;
590 }() ),
591
592 /**
593 * Client-side module loader which integrates with the MediaWiki ResourceLoader
594 * @class mw.loader
595 * @singleton
596 */
597 loader: ( function () {
598
599 /* Private Members */
600
601 /**
602 * Mapping of registered modules
603 *
604 * The jquery module is pre-registered, because it must have already
605 * been provided for this object to have been built, and in debug mode
606 * jquery would have been provided through a unique loader request,
607 * making it impossible to hold back registration of jquery until after
608 * mediawiki.
609 *
610 * For exact details on support for script, style and messages, look at
611 * mw.loader.implement.
612 *
613 * Format:
614 * {
615 * 'moduleName': {
616 * // At registry
617 * 'version': ############## (unix timestamp),
618 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
619 * 'group': 'somegroup', (or) null,
620 * 'source': 'local', 'someforeignwiki', (or) null
621 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
622 * 'skip': 'return !!window.Example', (or) null
623 *
624 * // Added during implementation
625 * 'skipped': true,
626 * 'script': ...,
627 * 'style': ...,
628 * 'messages': { 'key': 'value' },
629 * }
630 * }
631 *
632 * @property
633 * @private
634 */
635 var registry = {},
636 //
637 // Mapping of sources, keyed by source-id, values are strings.
638 // Format:
639 // {
640 // 'sourceId': 'http://foo.bar/w/load.php'
641 // }
642 //
643 sources = {},
644 // List of modules which will be loaded as when ready
645 batch = [],
646 // List of modules to be loaded
647 queue = [],
648 // List of callback functions waiting for modules to be ready to be called
649 jobs = [],
650 // Selector cache for the marker element. Use getMarker() to get/use the marker!
651 $marker = null,
652 // Buffer for addEmbeddedCSS.
653 cssBuffer = '',
654 // Callbacks for addEmbeddedCSS.
655 cssCallbacks = $.Callbacks();
656
657 /* Private methods */
658
659 function getMarker() {
660 // Cached
661 if ( !$marker ) {
662 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
663 if ( !$marker.length ) {
664 mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
665 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
666 }
667 }
668 return $marker;
669 }
670
671 /**
672 * Create a new style tag and add it to the DOM.
673 *
674 * @private
675 * @param {string} text CSS text
676 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
677 * inserted before. Otherwise it will be appended to `<head>`.
678 * @return {HTMLElement} Reference to the created `<style>` element.
679 */
680 function newStyleTag( text, nextnode ) {
681 var s = document.createElement( 'style' );
682 // Insert into document before setting cssText (bug 33305)
683 if ( nextnode ) {
684 // Must be inserted with native insertBefore, not $.fn.before.
685 // When using jQuery to insert it, like $nextnode.before( s ),
686 // then IE6 will throw "Access is denied" when trying to append
687 // to .cssText later. Some kind of weird security measure.
688 // http://stackoverflow.com/q/12586482/319266
689 // Works: jsfiddle.net/zJzMy/1
690 // Fails: jsfiddle.net/uJTQz
691 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
692 if ( nextnode.jquery ) {
693 nextnode = nextnode.get( 0 );
694 }
695 nextnode.parentNode.insertBefore( s, nextnode );
696 } else {
697 document.getElementsByTagName( 'head' )[0].appendChild( s );
698 }
699 if ( s.styleSheet ) {
700 // IE
701 s.styleSheet.cssText = text;
702 } else {
703 // Other browsers.
704 // (Safari sometimes borks on non-string values,
705 // play safe by casting to a string, just in case.)
706 s.appendChild( document.createTextNode( String( text ) ) );
707 }
708 return s;
709 }
710
711 /**
712 * Checks whether it is safe to add this css to a stylesheet.
713 *
714 * @private
715 * @param {string} cssText
716 * @return {boolean} False if a new one must be created.
717 */
718 function canExpandStylesheetWith( cssText ) {
719 // Makes sure that cssText containing `@import`
720 // rules will end up in a new stylesheet (as those only work when
721 // placed at the start of a stylesheet; bug 35562).
722 return cssText.indexOf( '@import' ) === -1;
723 }
724
725 /**
726 * Add a bit of CSS text to the current browser page.
727 *
728 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
729 * or create a new one based on whether the given `cssText` is safe for extension.
730 *
731 * @param {string} [cssText=cssBuffer] If called without cssText,
732 * the internal buffer will be inserted instead.
733 * @param {Function} [callback]
734 */
735 function addEmbeddedCSS( cssText, callback ) {
736 var $style, styleEl;
737
738 if ( callback ) {
739 cssCallbacks.add( callback );
740 }
741
742 // Yield once before inserting the <style> tag. There are likely
743 // more calls coming up which we can combine this way.
744 // Appending a stylesheet and waiting for the browser to repaint
745 // is fairly expensive, this reduces it (bug 45810)
746 if ( cssText ) {
747 // Be careful not to extend the buffer with css that needs a new stylesheet
748 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
749 // Linebreak for somewhat distinguishable sections
750 // (the rl-cachekey comment separating each)
751 cssBuffer += '\n' + cssText;
752 // TODO: Use requestAnimationFrame in the future which will
753 // perform even better by not injecting styles while the browser
754 // is paiting.
755 setTimeout( function () {
756 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
757 // (below version 13) has the non-standard behaviour of passing a
758 // numerical "lateness" value as first argument to this callback
759 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
760 addEmbeddedCSS();
761 } );
762 return;
763 }
764
765 // This is a delayed call and we got a buffer still
766 } else if ( cssBuffer ) {
767 cssText = cssBuffer;
768 cssBuffer = '';
769 } else {
770 // This is a delayed call, but buffer is already cleared by
771 // another delayed call.
772 return;
773 }
774
775 // By default, always create a new <style>. Appending text to a <style>
776 // tag is bad as it means the contents have to be re-parsed (bug 45810).
777 //
778 // Except, of course, in IE 9 and below. In there we default to re-using and
779 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
780 if ( 'documentMode' in document && document.documentMode <= 9 ) {
781
782 $style = getMarker().prev();
783 // Verify that the the element before Marker actually is a
784 // <style> tag and one that came from ResourceLoader
785 // (not some other style tag or even a `<meta>` or `<script>`).
786 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
787 // There's already a dynamic <style> tag present and
788 // canExpandStylesheetWith() gave a green light to append more to it.
789 styleEl = $style.get( 0 );
790 if ( styleEl.styleSheet ) {
791 try {
792 styleEl.styleSheet.cssText += cssText; // IE
793 } catch ( e ) {
794 log( 'Stylesheet error', e );
795 }
796 } else {
797 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
798 }
799 cssCallbacks.fire().empty();
800 return;
801 }
802 }
803
804 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
805
806 cssCallbacks.fire().empty();
807 }
808
809 /**
810 * Generates an ISO8601 "basic" string from a UNIX timestamp
811 * @private
812 */
813 function formatVersionNumber( timestamp ) {
814 var d = new Date();
815 function pad( a, b, c ) {
816 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
817 }
818 d.setTime( timestamp * 1000 );
819 return [
820 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
821 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
822 ].join( '' );
823 }
824
825 /**
826 * Resolves dependencies and detects circular references.
827 *
828 * @private
829 * @param {string} module Name of the top-level module whose dependencies shall be
830 * resolved and sorted.
831 * @param {Array} resolved Returns a topological sort of the given module and its
832 * dependencies, such that later modules depend on earlier modules. The array
833 * contains the module names. If the array contains already some module names,
834 * this function appends its result to the pre-existing array.
835 * @param {Object} [unresolved] Hash used to track the current dependency
836 * chain; used to report loops in the dependency graph.
837 * @throws {Error} If any unregistered module or a dependency loop is encountered
838 */
839 function sortDependencies( module, resolved, unresolved ) {
840 var n, deps, len, skip;
841
842 if ( registry[module] === undefined ) {
843 throw new Error( 'Unknown dependency: ' + module );
844 }
845
846 if ( registry[module].skip !== null ) {
847 /*jshint evil:true */
848 skip = new Function( registry[module].skip );
849 registry[module].skip = null;
850 if ( skip() ) {
851 registry[module].skipped = true;
852 registry[module].dependencies = [];
853 registry[module].state = 'ready';
854 handlePending( module );
855 return;
856 }
857 }
858
859 // Resolves dynamic loader function and replaces it with its own results
860 if ( $.isFunction( registry[module].dependencies ) ) {
861 registry[module].dependencies = registry[module].dependencies();
862 // Ensures the module's dependencies are always in an array
863 if ( typeof registry[module].dependencies !== 'object' ) {
864 registry[module].dependencies = [registry[module].dependencies];
865 }
866 }
867 if ( $.inArray( module, resolved ) !== -1 ) {
868 // Module already resolved; nothing to do.
869 return;
870 }
871 // unresolved is optional, supply it if not passed in
872 if ( !unresolved ) {
873 unresolved = {};
874 }
875 // Tracks down dependencies
876 deps = registry[module].dependencies;
877 len = deps.length;
878 for ( n = 0; n < len; n += 1 ) {
879 if ( $.inArray( deps[n], resolved ) === -1 ) {
880 if ( unresolved[deps[n]] ) {
881 throw new Error(
882 'Circular reference detected: ' + module +
883 ' -> ' + deps[n]
884 );
885 }
886
887 // Add to unresolved
888 unresolved[module] = true;
889 sortDependencies( deps[n], resolved, unresolved );
890 delete unresolved[module];
891 }
892 }
893 resolved[resolved.length] = module;
894 }
895
896 /**
897 * Gets a list of module names that a module depends on in their proper dependency
898 * order.
899 *
900 * @private
901 * @param {string} module Module name or array of string module names
902 * @return {Array} list of dependencies, including 'module'.
903 * @throws {Error} If circular reference is detected
904 */
905 function resolve( module ) {
906 var m, resolved;
907
908 // Allow calling with an array of module names
909 if ( $.isArray( module ) ) {
910 resolved = [];
911 for ( m = 0; m < module.length; m += 1 ) {
912 sortDependencies( module[m], resolved );
913 }
914 return resolved;
915 }
916
917 if ( typeof module === 'string' ) {
918 resolved = [];
919 sortDependencies( module, resolved );
920 return resolved;
921 }
922
923 throw new Error( 'Invalid module argument: ' + module );
924 }
925
926 /**
927 * Narrows a list of module names down to those matching a specific
928 * state (see comment on top of this scope for a list of valid states).
929 * One can also filter for 'unregistered', which will return the
930 * modules names that don't have a registry entry.
931 *
932 * @private
933 * @param {string|string[]} states Module states to filter by
934 * @param {Array} [modules] List of module names to filter (optional, by default the entire
935 * registry is used)
936 * @return {Array} List of filtered module names
937 */
938 function filter( states, modules ) {
939 var list, module, s, m;
940
941 // Allow states to be given as a string
942 if ( typeof states === 'string' ) {
943 states = [states];
944 }
945 // If called without a list of modules, build and use a list of all modules
946 list = [];
947 if ( modules === undefined ) {
948 modules = [];
949 for ( module in registry ) {
950 modules[modules.length] = module;
951 }
952 }
953 // Build a list of modules which are in one of the specified states
954 for ( s = 0; s < states.length; s += 1 ) {
955 for ( m = 0; m < modules.length; m += 1 ) {
956 if ( registry[modules[m]] === undefined ) {
957 // Module does not exist
958 if ( states[s] === 'unregistered' ) {
959 // OK, undefined
960 list[list.length] = modules[m];
961 }
962 } else {
963 // Module exists, check state
964 if ( registry[modules[m]].state === states[s] ) {
965 // OK, correct state
966 list[list.length] = modules[m];
967 }
968 }
969 }
970 }
971 return list;
972 }
973
974 /**
975 * Determine whether all dependencies are in state 'ready', which means we may
976 * execute the module or job now.
977 *
978 * @private
979 * @param {Array} dependencies Dependencies (module names) to be checked.
980 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
981 */
982 function allReady( dependencies ) {
983 return filter( 'ready', dependencies ).length === dependencies.length;
984 }
985
986 /**
987 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
988 * and modules that depend upon this module. if the given module failed, propagate the 'error'
989 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
990 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
991 *
992 * @private
993 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
994 */
995 function handlePending( module ) {
996 var j, job, hasErrors, m, stateChange;
997
998 // Modules.
999 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
1000 // If the current module failed, mark all dependent modules also as failed.
1001 // Iterate until steady-state to propagate the error state upwards in the
1002 // dependency tree.
1003 do {
1004 stateChange = false;
1005 for ( m in registry ) {
1006 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
1007 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
1008 registry[m].state = 'error';
1009 stateChange = true;
1010 }
1011 }
1012 }
1013 } while ( stateChange );
1014 }
1015
1016 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1017 for ( j = 0; j < jobs.length; j += 1 ) {
1018 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
1019 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1020 // All dependencies satisfied, or some have errors
1021 job = jobs[j];
1022 jobs.splice( j, 1 );
1023 j -= 1;
1024 try {
1025 if ( hasErrors ) {
1026 if ( $.isFunction( job.error ) ) {
1027 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1028 }
1029 } else {
1030 if ( $.isFunction( job.ready ) ) {
1031 job.ready();
1032 }
1033 }
1034 } catch ( e ) {
1035 // A user-defined callback raised an exception.
1036 // Swallow it to protect our state machine!
1037 log( 'Exception thrown by user callback', e );
1038 }
1039 }
1040 }
1041
1042 if ( registry[module].state === 'ready' ) {
1043 // The current module became 'ready'. Set it in the module store, and recursively execute all
1044 // dependent modules that are loaded and now have all dependencies satisfied.
1045 mw.loader.store.set( module, registry[module] );
1046 for ( m in registry ) {
1047 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1048 execute( m );
1049 }
1050 }
1051 }
1052 }
1053
1054 /**
1055 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1056 * depending on whether document-ready has occurred yet and whether we are in async mode.
1057 *
1058 * @private
1059 * @param {string} src URL to script, will be used as the src attribute in the script tag
1060 * @param {Function} [callback] Callback which will be run when the script is done
1061 * @param {boolean} [async=false] Whether to load modules asynchronously.
1062 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1063 */
1064 function addScript( src, callback, async ) {
1065 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1066 if ( $.isReady || async ) {
1067 $.ajax( {
1068 url: src,
1069 dataType: 'script',
1070 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1071 // XHR for a same domain request instead of <script>, which changes the request
1072 // headers (potentially missing a cache hit), and reduces caching in general
1073 // since browsers cache XHR much less (if at all). And XHR means we retreive
1074 // text, so we'd need to $.globalEval, which then messes up line numbers.
1075 crossDomain: true,
1076 cache: true,
1077 async: true
1078 } ).always( callback );
1079 } else {
1080 /*jshint evil:true */
1081 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1082 if ( callback ) {
1083 // Document.write is synchronous, so this is called when it's done.
1084 // FIXME: That's a lie. doc.write isn't actually synchronous.
1085 callback();
1086 }
1087 }
1088 }
1089
1090 /**
1091 * Executes a loaded module, making it ready to use
1092 *
1093 * @private
1094 * @param {string} module Module name to execute
1095 */
1096 function execute( module ) {
1097 var key, value, media, i, urls, cssHandle, checkCssHandles,
1098 cssHandlesRegistered = false;
1099
1100 if ( registry[module] === undefined ) {
1101 throw new Error( 'Module has not been registered yet: ' + module );
1102 } else if ( registry[module].state === 'registered' ) {
1103 throw new Error( 'Module has not been requested from the server yet: ' + module );
1104 } else if ( registry[module].state === 'loading' ) {
1105 throw new Error( 'Module has not completed loading yet: ' + module );
1106 } else if ( registry[module].state === 'ready' ) {
1107 throw new Error( 'Module has already been executed: ' + module );
1108 }
1109
1110 /**
1111 * Define loop-function here for efficiency
1112 * and to avoid re-using badly scoped variables.
1113 * @ignore
1114 */
1115 function addLink( media, url ) {
1116 var el = document.createElement( 'link' );
1117 // For IE: Insert in document *before* setting href
1118 getMarker().before( el );
1119 el.rel = 'stylesheet';
1120 if ( media && media !== 'all' ) {
1121 el.media = media;
1122 }
1123 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1124 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1125 el.href = url;
1126 }
1127
1128 function runScript() {
1129 var script, markModuleReady, nestedAddScript;
1130 try {
1131 script = registry[module].script;
1132 markModuleReady = function () {
1133 registry[module].state = 'ready';
1134 handlePending( module );
1135 };
1136 nestedAddScript = function ( arr, callback, async, i ) {
1137 // Recursively call addScript() in its own callback
1138 // for each element of arr.
1139 if ( i >= arr.length ) {
1140 // We're at the end of the array
1141 callback();
1142 return;
1143 }
1144
1145 addScript( arr[i], function () {
1146 nestedAddScript( arr, callback, async, i + 1 );
1147 }, async );
1148 };
1149
1150 if ( $.isArray( script ) ) {
1151 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1152 } else if ( $.isFunction( script ) ) {
1153 registry[module].state = 'ready';
1154 // Pass jQuery twice so that the signature of the closure which wraps
1155 // the script can bind both '$' and 'jQuery'.
1156 script( $, $ );
1157 handlePending( module );
1158 }
1159 } catch ( e ) {
1160 // This needs to NOT use mw.log because these errors are common in production mode
1161 // and not in debug mode, such as when a symbol that should be global isn't exported
1162 log( 'Exception thrown by ' + module, e );
1163 registry[module].state = 'error';
1164 handlePending( module );
1165 }
1166 }
1167
1168 // This used to be inside runScript, but since that is now fired asychronously
1169 // (after CSS is loaded) we need to set it here right away. It is crucial that
1170 // when execute() is called this is set synchronously, otherwise modules will get
1171 // executed multiple times as the registry will state that it isn't loading yet.
1172 registry[module].state = 'loading';
1173
1174 // Add localizations to message system
1175 if ( $.isPlainObject( registry[module].messages ) ) {
1176 mw.messages.set( registry[module].messages );
1177 }
1178
1179 // Initialise templates
1180 if ( registry[module].templates ) {
1181 mw.templates.set( module, registry[module].templates );
1182 }
1183
1184 if ( $.isReady || registry[module].async ) {
1185 // Make sure we don't run the scripts until all (potentially asynchronous)
1186 // stylesheet insertions have completed.
1187 ( function () {
1188 var pending = 0;
1189 checkCssHandles = function () {
1190 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1191 // one of the cssHandles is fired while we're still creating more handles.
1192 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1193 runScript();
1194 runScript = undefined; // Revoke
1195 }
1196 };
1197 cssHandle = function () {
1198 var check = checkCssHandles;
1199 pending++;
1200 return function () {
1201 if (check) {
1202 pending--;
1203 check();
1204 check = undefined; // Revoke
1205 }
1206 };
1207 };
1208 }() );
1209 } else {
1210 // We are in blocking mode, and so we can't afford to wait for CSS
1211 cssHandle = function () {};
1212 // Run immediately
1213 checkCssHandles = runScript;
1214 }
1215
1216 // Process styles (see also mw.loader.implement)
1217 // * back-compat: { <media>: css }
1218 // * back-compat: { <media>: [url, ..] }
1219 // * { "css": [css, ..] }
1220 // * { "url": { <media>: [url, ..] } }
1221 if ( $.isPlainObject( registry[module].style ) ) {
1222 for ( key in registry[module].style ) {
1223 value = registry[module].style[key];
1224 media = undefined;
1225
1226 if ( key !== 'url' && key !== 'css' ) {
1227 // Backwards compatibility, key is a media-type
1228 if ( typeof value === 'string' ) {
1229 // back-compat: { <media>: css }
1230 // Ignore 'media' because it isn't supported (nor was it used).
1231 // Strings are pre-wrapped in "@media". The media-type was just ""
1232 // (because it had to be set to something).
1233 // This is one of the reasons why this format is no longer used.
1234 addEmbeddedCSS( value, cssHandle() );
1235 } else {
1236 // back-compat: { <media>: [url, ..] }
1237 media = key;
1238 key = 'bc-url';
1239 }
1240 }
1241
1242 // Array of css strings in key 'css',
1243 // or back-compat array of urls from media-type
1244 if ( $.isArray( value ) ) {
1245 for ( i = 0; i < value.length; i += 1 ) {
1246 if ( key === 'bc-url' ) {
1247 // back-compat: { <media>: [url, ..] }
1248 addLink( media, value[i] );
1249 } else if ( key === 'css' ) {
1250 // { "css": [css, ..] }
1251 addEmbeddedCSS( value[i], cssHandle() );
1252 }
1253 }
1254 // Not an array, but a regular object
1255 // Array of urls inside media-type key
1256 } else if ( typeof value === 'object' ) {
1257 // { "url": { <media>: [url, ..] } }
1258 for ( media in value ) {
1259 urls = value[media];
1260 for ( i = 0; i < urls.length; i += 1 ) {
1261 addLink( media, urls[i] );
1262 }
1263 }
1264 }
1265 }
1266 }
1267
1268 // Kick off.
1269 cssHandlesRegistered = true;
1270 checkCssHandles();
1271 }
1272
1273 /**
1274 * Adds a dependencies to the queue with optional callbacks to be run
1275 * when the dependencies are ready or fail
1276 *
1277 * @private
1278 * @param {string|string[]} dependencies Module name or array of string module names
1279 * @param {Function} [ready] Callback to execute when all dependencies are ready
1280 * @param {Function} [error] Callback to execute when any dependency fails
1281 * @param {boolean} [async=false] Whether to load modules asynchronously.
1282 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1283 */
1284 function request( dependencies, ready, error, async ) {
1285 var n;
1286
1287 // Allow calling by single module name
1288 if ( typeof dependencies === 'string' ) {
1289 dependencies = [dependencies];
1290 }
1291
1292 // Add ready and error callbacks if they were given
1293 if ( ready !== undefined || error !== undefined ) {
1294 jobs[jobs.length] = {
1295 'dependencies': filter(
1296 ['registered', 'loading', 'loaded'],
1297 dependencies
1298 ),
1299 'ready': ready,
1300 'error': error
1301 };
1302 }
1303
1304 // Queue up any dependencies that are registered
1305 dependencies = filter( ['registered'], dependencies );
1306 for ( n = 0; n < dependencies.length; n += 1 ) {
1307 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1308 queue[queue.length] = dependencies[n];
1309 if ( async ) {
1310 // Mark this module as async in the registry
1311 registry[dependencies[n]].async = true;
1312 }
1313 }
1314 }
1315
1316 // Work the queue
1317 mw.loader.work();
1318 }
1319
1320 function sortQuery( o ) {
1321 var sorted = {}, key, a = [];
1322 for ( key in o ) {
1323 if ( hasOwn.call( o, key ) ) {
1324 a.push( key );
1325 }
1326 }
1327 a.sort();
1328 for ( key = 0; key < a.length; key += 1 ) {
1329 sorted[a[key]] = o[a[key]];
1330 }
1331 return sorted;
1332 }
1333
1334 /**
1335 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1336 * to a query string of the form foo.bar,baz|bar.baz,quux
1337 * @private
1338 */
1339 function buildModulesString( moduleMap ) {
1340 var arr = [], p, prefix;
1341 for ( prefix in moduleMap ) {
1342 p = prefix === '' ? '' : prefix + '.';
1343 arr.push( p + moduleMap[prefix].join( ',' ) );
1344 }
1345 return arr.join( '|' );
1346 }
1347
1348 /**
1349 * Asynchronously append a script tag to the end of the body
1350 * that invokes load.php
1351 * @private
1352 * @param {Object} moduleMap Module map, see #buildModulesString
1353 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1354 * @param {string} sourceLoadScript URL of load.php
1355 * @param {boolean} async Whether to load modules asynchronously.
1356 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1357 */
1358 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1359 var request = $.extend(
1360 { modules: buildModulesString( moduleMap ) },
1361 currReqBase
1362 );
1363 request = sortQuery( request );
1364 // Append &* to avoid triggering the IE6 extension check
1365 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1366 }
1367
1368 /* Public Members */
1369 return {
1370 /**
1371 * The module registry is exposed as an aid for debugging and inspecting page
1372 * state; it is not a public interface for modifying the registry.
1373 *
1374 * @see #registry
1375 * @property
1376 * @private
1377 */
1378 moduleRegistry: registry,
1379
1380 /**
1381 * @inheritdoc #newStyleTag
1382 * @method
1383 */
1384 addStyleTag: newStyleTag,
1385
1386 /**
1387 * Batch-request queued dependencies from the server.
1388 */
1389 work: function () {
1390 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1391 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1392 currReqBase, currReqBaseLength, moduleMap, l,
1393 lastDotIndex, prefix, suffix, bytesAdded, async;
1394
1395 // Build a list of request parameters common to all requests.
1396 reqBase = {
1397 skin: mw.config.get( 'skin' ),
1398 lang: mw.config.get( 'wgUserLanguage' ),
1399 debug: mw.config.get( 'debug' )
1400 };
1401 // Split module batch by source and by group.
1402 splits = {};
1403 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1404
1405 // Appends a list of modules from the queue to the batch
1406 for ( q = 0; q < queue.length; q += 1 ) {
1407 // Only request modules which are registered
1408 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1409 // Prevent duplicate entries
1410 if ( $.inArray( queue[q], batch ) === -1 ) {
1411 batch[batch.length] = queue[q];
1412 // Mark registered modules as loading
1413 registry[queue[q]].state = 'loading';
1414 }
1415 }
1416 }
1417
1418 mw.loader.store.init();
1419 if ( mw.loader.store.enabled ) {
1420 concatSource = [];
1421 origBatch = batch;
1422 batch = $.grep( batch, function ( module ) {
1423 var source = mw.loader.store.get( module );
1424 if ( source ) {
1425 concatSource.push( source );
1426 return false;
1427 }
1428 return true;
1429 } );
1430 try {
1431 $.globalEval( concatSource.join( ';' ) );
1432 } catch ( err ) {
1433 // Not good, the cached mw.loader.implement calls failed! This should
1434 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1435 // Depending on how corrupt the string is, it is likely that some
1436 // modules' implement() succeeded while the ones after the error will
1437 // never run and leave their modules in the 'loading' state forever.
1438
1439 // Since this is an error not caused by an individual module but by
1440 // something that infected the implement call itself, don't take any
1441 // risks and clear everything in this cache.
1442 mw.loader.store.clear();
1443 // Re-add the ones still pending back to the batch and let the server
1444 // repopulate these modules to the cache.
1445 // This means that at most one module will be useless (the one that had
1446 // the error) instead of all of them.
1447 log( 'Error while evaluating data from mw.loader.store', err );
1448 origBatch = $.grep( origBatch, function ( module ) {
1449 return registry[module].state === 'loading';
1450 } );
1451 batch = batch.concat( origBatch );
1452 }
1453 }
1454
1455 // Early exit if there's nothing to load...
1456 if ( !batch.length ) {
1457 return;
1458 }
1459
1460 // The queue has been processed into the batch, clear up the queue.
1461 queue = [];
1462
1463 // Always order modules alphabetically to help reduce cache
1464 // misses for otherwise identical content.
1465 batch.sort();
1466
1467 // Split batch by source and by group.
1468 for ( b = 0; b < batch.length; b += 1 ) {
1469 bSource = registry[batch[b]].source;
1470 bGroup = registry[batch[b]].group;
1471 if ( splits[bSource] === undefined ) {
1472 splits[bSource] = {};
1473 }
1474 if ( splits[bSource][bGroup] === undefined ) {
1475 splits[bSource][bGroup] = [];
1476 }
1477 bSourceGroup = splits[bSource][bGroup];
1478 bSourceGroup[bSourceGroup.length] = batch[b];
1479 }
1480
1481 // Clear the batch - this MUST happen before we append any
1482 // script elements to the body or it's possible that a script
1483 // will be locally cached, instantly load, and work the batch
1484 // again, all before we've cleared it causing each request to
1485 // include modules which are already loaded.
1486 batch = [];
1487
1488 for ( source in splits ) {
1489
1490 sourceLoadScript = sources[source];
1491
1492 for ( group in splits[source] ) {
1493
1494 // Cache access to currently selected list of
1495 // modules for this group from this source.
1496 modules = splits[source][group];
1497
1498 // Calculate the highest timestamp
1499 maxVersion = 0;
1500 for ( g = 0; g < modules.length; g += 1 ) {
1501 if ( registry[modules[g]].version > maxVersion ) {
1502 maxVersion = registry[modules[g]].version;
1503 }
1504 }
1505
1506 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1507 // For user modules append a user name to the request.
1508 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1509 currReqBase.user = mw.config.get( 'wgUserName' );
1510 }
1511 currReqBaseLength = $.param( currReqBase ).length;
1512 async = true;
1513 // We may need to split up the request to honor the query string length limit,
1514 // so build it piece by piece.
1515 l = currReqBaseLength + 9; // '&modules='.length == 9
1516
1517 moduleMap = {}; // { prefix: [ suffixes ] }
1518
1519 for ( i = 0; i < modules.length; i += 1 ) {
1520 // Determine how many bytes this module would add to the query string
1521 lastDotIndex = modules[i].lastIndexOf( '.' );
1522
1523 // If lastDotIndex is -1, substr() returns an empty string
1524 prefix = modules[i].substr( 0, lastDotIndex );
1525 suffix = modules[i].slice( lastDotIndex + 1 );
1526
1527 bytesAdded = moduleMap[prefix] !== undefined
1528 ? suffix.length + 3 // '%2C'.length == 3
1529 : modules[i].length + 3; // '%7C'.length == 3
1530
1531 // If the request would become too long, create a new one,
1532 // but don't create empty requests
1533 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1534 // This request would become too long, create a new one
1535 // and fire off the old one
1536 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1537 moduleMap = {};
1538 async = true;
1539 l = currReqBaseLength + 9;
1540 }
1541 if ( moduleMap[prefix] === undefined ) {
1542 moduleMap[prefix] = [];
1543 }
1544 moduleMap[prefix].push( suffix );
1545 if ( !registry[modules[i]].async ) {
1546 // If this module is blocking, make the entire request blocking
1547 // This is slightly suboptimal, but in practice mixing of blocking
1548 // and async modules will only occur in debug mode.
1549 async = false;
1550 }
1551 l += bytesAdded;
1552 }
1553 // If there's anything left in moduleMap, request that too
1554 if ( !$.isEmptyObject( moduleMap ) ) {
1555 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1556 }
1557 }
1558 }
1559 },
1560
1561 /**
1562 * Register a source.
1563 *
1564 * The #work method will use this information to split up requests by source.
1565 *
1566 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1567 *
1568 * @param {string} id Short string representing a source wiki, used internally for
1569 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1570 * @param {Object|string} loadUrl load.php url, may be an object for backwards-compatibility
1571 * @return {boolean}
1572 */
1573 addSource: function ( id, loadUrl ) {
1574 var source;
1575 // Allow multiple additions
1576 if ( typeof id === 'object' ) {
1577 for ( source in id ) {
1578 mw.loader.addSource( source, id[source] );
1579 }
1580 return true;
1581 }
1582
1583 if ( sources[id] !== undefined ) {
1584 throw new Error( 'source already registered: ' + id );
1585 }
1586
1587 if ( typeof loadUrl === 'object' ) {
1588 loadUrl = loadUrl.loadScript;
1589 }
1590
1591 sources[id] = loadUrl;
1592
1593 return true;
1594 },
1595
1596 /**
1597 * Register a module, letting the system know about it and its
1598 * properties. Startup modules contain calls to this function.
1599 *
1600 * @param {string} module Module name
1601 * @param {number} version Module version number as a timestamp (falls backs to 0)
1602 * @param {string|Array|Function} dependencies One string or array of strings of module
1603 * names on which this module depends, or a function that returns that array.
1604 * @param {string} [group=null] Group which the module is in
1605 * @param {string} [source='local'] Name of the source
1606 * @param {string} [skip=null] Script body of the skip function
1607 */
1608 register: function ( module, version, dependencies, group, source, skip ) {
1609 var m;
1610 // Allow multiple registration
1611 if ( typeof module === 'object' ) {
1612 for ( m = 0; m < module.length; m += 1 ) {
1613 // module is an array of module names
1614 if ( typeof module[m] === 'string' ) {
1615 mw.loader.register( module[m] );
1616 // module is an array of arrays
1617 } else if ( typeof module[m] === 'object' ) {
1618 mw.loader.register.apply( mw.loader, module[m] );
1619 }
1620 }
1621 return;
1622 }
1623 // Validate input
1624 if ( typeof module !== 'string' ) {
1625 throw new Error( 'module must be a string, not a ' + typeof module );
1626 }
1627 if ( registry[module] !== undefined ) {
1628 throw new Error( 'module already registered: ' + module );
1629 }
1630 // List the module as registered
1631 registry[module] = {
1632 version: version !== undefined ? parseInt( version, 10 ) : 0,
1633 dependencies: [],
1634 group: typeof group === 'string' ? group : null,
1635 source: typeof source === 'string' ? source : 'local',
1636 state: 'registered',
1637 skip: typeof skip === 'string' ? skip : null
1638 };
1639 if ( typeof dependencies === 'string' ) {
1640 // Allow dependencies to be given as a single module name
1641 registry[module].dependencies = [ dependencies ];
1642 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1643 // Allow dependencies to be given as an array of module names
1644 // or a function which returns an array
1645 registry[module].dependencies = dependencies;
1646 }
1647 },
1648
1649 /**
1650 * Implement a module given the components that make up the module.
1651 *
1652 * When #load or #using requests one or more modules, the server
1653 * response contain calls to this function.
1654 *
1655 * All arguments are required.
1656 *
1657 * @param {string} module Name of module
1658 * @param {Function|Array} script Function with module code or Array of URLs to
1659 * be used as the src attribute of a new `<script>` tag.
1660 * @param {Object} style Should follow one of the following patterns:
1661 *
1662 * { "css": [css, ..] }
1663 * { "url": { <media>: [url, ..] } }
1664 *
1665 * And for backwards compatibility (needs to be supported forever due to caching):
1666 *
1667 * { <media>: css }
1668 * { <media>: [url, ..] }
1669 *
1670 * The reason css strings are not concatenated anymore is bug 31676. We now check
1671 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1672 *
1673 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
1674 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1675 */
1676 implement: function ( module, script, style, msgs, templates ) {
1677 // Validate input
1678 if ( typeof module !== 'string' ) {
1679 throw new Error( 'module must be a string, not a ' + typeof module );
1680 }
1681 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1682 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1683 }
1684 if ( !$.isPlainObject( style ) ) {
1685 throw new Error( 'style must be an object, not a ' + typeof style );
1686 }
1687 if ( !$.isPlainObject( msgs ) ) {
1688 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1689 }
1690 if ( templates !== undefined && !$.isPlainObject( templates ) ) {
1691 throw new Error( 'templates must be an object, not a ' + typeof templates );
1692 }
1693 // Automatically register module
1694 if ( registry[module] === undefined ) {
1695 mw.loader.register( module );
1696 }
1697 // Check for duplicate implementation
1698 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1699 throw new Error( 'module already implemented: ' + module );
1700 }
1701 // Attach components
1702 registry[module].script = script;
1703 registry[module].style = style;
1704 registry[module].messages = msgs;
1705 // Templates are optional (for back-compat)
1706 registry[module].templates = templates || {};
1707 // The module may already have been marked as erroneous
1708 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1709 registry[module].state = 'loaded';
1710 if ( allReady( registry[module].dependencies ) ) {
1711 execute( module );
1712 }
1713 }
1714 },
1715
1716 /**
1717 * Execute a function as soon as one or more required modules are ready.
1718 *
1719 * Example of inline dependency on OOjs:
1720 *
1721 * mw.loader.using( 'oojs', function () {
1722 * OO.compare( [ 1 ], [ 1 ] );
1723 * } );
1724 *
1725 * @param {string|Array} dependencies Module name or array of modules names the callback
1726 * dependends on to be ready before executing
1727 * @param {Function} [ready] Callback to execute when all dependencies are ready
1728 * @param {Function} [error] Callback to execute if one or more dependencies failed
1729 * @return {jQuery.Promise}
1730 * @since 1.23 this returns a promise
1731 */
1732 using: function ( dependencies, ready, error ) {
1733 var deferred = $.Deferred();
1734
1735 // Allow calling with a single dependency as a string
1736 if ( typeof dependencies === 'string' ) {
1737 dependencies = [ dependencies ];
1738 } else if ( !$.isArray( dependencies ) ) {
1739 // Invalid input
1740 throw new Error( 'Dependencies must be a string or an array' );
1741 }
1742
1743 if ( ready ) {
1744 deferred.done( ready );
1745 }
1746 if ( error ) {
1747 deferred.fail( error );
1748 }
1749
1750 // Resolve entire dependency map
1751 dependencies = resolve( dependencies );
1752 if ( allReady( dependencies ) ) {
1753 // Run ready immediately
1754 deferred.resolve();
1755 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1756 // Execute error immediately if any dependencies have errors
1757 deferred.reject(
1758 new Error( 'One or more dependencies failed to load' ),
1759 dependencies
1760 );
1761 } else {
1762 // Not all dependencies are ready: queue up a request
1763 request( dependencies, deferred.resolve, deferred.reject );
1764 }
1765
1766 return deferred.promise();
1767 },
1768
1769 /**
1770 * Load an external script or one or more modules.
1771 *
1772 * @param {string|Array} modules Either the name of a module, array of modules,
1773 * or a URL of an external script or style
1774 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1775 * external script or style; acceptable values are "text/css" and
1776 * "text/javascript"; if no type is provided, text/javascript is assumed.
1777 * @param {boolean} [async] Whether to load modules asynchronously.
1778 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1779 * Defaults to `true` if loading a URL, `false` otherwise.
1780 */
1781 load: function ( modules, type, async ) {
1782 var filtered, m, module, l;
1783
1784 // Validate input
1785 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1786 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1787 }
1788 // Allow calling with an external url or single dependency as a string
1789 if ( typeof modules === 'string' ) {
1790 // Support adding arbitrary external scripts
1791 if ( /^(https?:)?\/\//.test( modules ) ) {
1792 if ( async === undefined ) {
1793 // Assume async for bug 34542
1794 async = true;
1795 }
1796 if ( type === 'text/css' ) {
1797 // IE7-8 throws security warnings when inserting a <link> tag
1798 // with a protocol-relative URL set though attributes (instead of
1799 // properties) - when on HTTPS. See also bug 41331.
1800 l = document.createElement( 'link' );
1801 l.rel = 'stylesheet';
1802 l.href = modules;
1803 $( 'head' ).append( l );
1804 return;
1805 }
1806 if ( type === 'text/javascript' || type === undefined ) {
1807 addScript( modules, null, async );
1808 return;
1809 }
1810 // Unknown type
1811 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1812 }
1813 // Called with single module
1814 modules = [ modules ];
1815 }
1816
1817 // Filter out undefined modules, otherwise resolve() will throw
1818 // an exception for trying to load an undefined module.
1819 // Undefined modules are acceptable here in load(), because load() takes
1820 // an array of unrelated modules, whereas the modules passed to
1821 // using() are related and must all be loaded.
1822 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1823 module = registry[modules[m]];
1824 if ( module !== undefined ) {
1825 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1826 filtered[filtered.length] = modules[m];
1827 }
1828 }
1829 }
1830
1831 if ( filtered.length === 0 ) {
1832 return;
1833 }
1834 // Resolve entire dependency map
1835 filtered = resolve( filtered );
1836 // If all modules are ready, nothing to be done
1837 if ( allReady( filtered ) ) {
1838 return;
1839 }
1840 // If any modules have errors: also quit.
1841 if ( filter( ['error', 'missing'], filtered ).length ) {
1842 return;
1843 }
1844 // Since some modules are not yet ready, queue up a request.
1845 request( filtered, undefined, undefined, async );
1846 },
1847
1848 /**
1849 * Change the state of one or more modules.
1850 *
1851 * @param {string|Object} module Module name or object of module name/state pairs
1852 * @param {string} state State name
1853 */
1854 state: function ( module, state ) {
1855 var m;
1856
1857 if ( typeof module === 'object' ) {
1858 for ( m in module ) {
1859 mw.loader.state( m, module[m] );
1860 }
1861 return;
1862 }
1863 if ( registry[module] === undefined ) {
1864 mw.loader.register( module );
1865 }
1866 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1867 && registry[module].state !== state ) {
1868 // Make sure pending modules depending on this one get executed if their
1869 // dependencies are now fulfilled!
1870 registry[module].state = state;
1871 handlePending( module );
1872 } else {
1873 registry[module].state = state;
1874 }
1875 },
1876
1877 /**
1878 * Get the version of a module.
1879 *
1880 * @param {string} module Name of module to get version for
1881 * @return {string|null} The version, or null if the module (or its version) is not
1882 * in the registry.
1883 */
1884 getVersion: function ( module ) {
1885 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1886 return formatVersionNumber( registry[module].version );
1887 }
1888 return null;
1889 },
1890
1891 /**
1892 * Get the state of a module.
1893 *
1894 * @param {string} module Name of module to get state for
1895 */
1896 getState: function ( module ) {
1897 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1898 return registry[module].state;
1899 }
1900 return null;
1901 },
1902
1903 /**
1904 * Get the names of all registered modules.
1905 *
1906 * @return {Array}
1907 */
1908 getModuleNames: function () {
1909 return $.map( registry, function ( i, key ) {
1910 return key;
1911 } );
1912 },
1913
1914 /**
1915 * @inheritdoc mw.inspect#runReports
1916 * @method
1917 */
1918 inspect: function () {
1919 var args = slice.call( arguments );
1920 mw.loader.using( 'mediawiki.inspect', function () {
1921 mw.inspect.runReports.apply( mw.inspect, args );
1922 } );
1923 },
1924
1925 /**
1926 * On browsers that implement the localStorage API, the module store serves as a
1927 * smart complement to the browser cache. Unlike the browser cache, the module store
1928 * can slice a concatenated response from ResourceLoader into its constituent
1929 * modules and cache each of them separately, using each module's versioning scheme
1930 * to determine when the cache should be invalidated.
1931 *
1932 * @singleton
1933 * @class mw.loader.store
1934 */
1935 store: {
1936 // Whether the store is in use on this page.
1937 enabled: null,
1938
1939 // The contents of the store, mapping '[module name]@[version]' keys
1940 // to module implementations.
1941 items: {},
1942
1943 // Cache hit stats
1944 stats: { hits: 0, misses: 0, expired: 0 },
1945
1946 /**
1947 * Construct a JSON-serializable object representing the content of the store.
1948 * @return {Object} Module store contents.
1949 */
1950 toJSON: function () {
1951 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
1952 },
1953
1954 /**
1955 * Get the localStorage key for the entire module store. The key references
1956 * $wgDBname to prevent clashes between wikis which share a common host.
1957 *
1958 * @return {string} localStorage item key
1959 */
1960 getStoreKey: function () {
1961 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
1962 },
1963
1964 /**
1965 * Get a string key on which to vary the module cache.
1966 * @return {string} String of concatenated vary conditions.
1967 */
1968 getVary: function () {
1969 return [
1970 mw.config.get( 'skin' ),
1971 mw.config.get( 'wgResourceLoaderStorageVersion' ),
1972 mw.config.get( 'wgUserLanguage' )
1973 ].join( ':' );
1974 },
1975
1976 /**
1977 * Get a string key for a specific module. The key format is '[name]@[version]'.
1978 *
1979 * @param {string} module Module name
1980 * @return {string|null} Module key or null if module does not exist
1981 */
1982 getModuleKey: function ( module ) {
1983 return typeof registry[module] === 'object' ?
1984 ( module + '@' + registry[module].version ) : null;
1985 },
1986
1987 /**
1988 * Initialize the store.
1989 *
1990 * Retrieves store from localStorage and (if successfully retrieved) decoding
1991 * the stored JSON value to a plain object.
1992 *
1993 * The try / catch block is used for JSON & localStorage feature detection.
1994 * See the in-line documentation for Modernizr's localStorage feature detection
1995 * code for a full account of why we need a try / catch:
1996 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
1997 */
1998 init: function () {
1999 var raw, data;
2000
2001 if ( mw.loader.store.enabled !== null ) {
2002 // Init already ran
2003 return;
2004 }
2005
2006 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
2007 // Disabled by configuration, or because debug mode is set
2008 mw.loader.store.enabled = false;
2009 return;
2010 }
2011
2012 try {
2013 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2014 // If we get here, localStorage is available; mark enabled
2015 mw.loader.store.enabled = true;
2016 data = JSON.parse( raw );
2017 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2018 mw.loader.store.items = data.items;
2019 return;
2020 }
2021 } catch ( e ) {
2022 log( 'Storage error', e );
2023 }
2024
2025 if ( raw === undefined ) {
2026 // localStorage failed; disable store
2027 mw.loader.store.enabled = false;
2028 } else {
2029 mw.loader.store.update();
2030 }
2031 },
2032
2033 /**
2034 * Retrieve a module from the store and update cache hit stats.
2035 *
2036 * @param {string} module Module name
2037 * @return {string|boolean} Module implementation or false if unavailable
2038 */
2039 get: function ( module ) {
2040 var key;
2041
2042 if ( !mw.loader.store.enabled ) {
2043 return false;
2044 }
2045
2046 key = mw.loader.store.getModuleKey( module );
2047 if ( key in mw.loader.store.items ) {
2048 mw.loader.store.stats.hits++;
2049 return mw.loader.store.items[key];
2050 }
2051 mw.loader.store.stats.misses++;
2052 return false;
2053 },
2054
2055 /**
2056 * Stringify a module and queue it for storage.
2057 *
2058 * @param {string} module Module name
2059 * @param {Object} descriptor The module's descriptor as set in the registry
2060 */
2061 set: function ( module, descriptor ) {
2062 var args, key;
2063
2064 if ( !mw.loader.store.enabled ) {
2065 return false;
2066 }
2067
2068 key = mw.loader.store.getModuleKey( module );
2069
2070 if (
2071 // Already stored a copy of this exact version
2072 key in mw.loader.store.items ||
2073 // Module failed to load
2074 descriptor.state !== 'ready' ||
2075 // Unversioned, private, or site-/user-specific
2076 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2077 // Partial descriptor
2078 $.inArray( undefined, [ descriptor.script, descriptor.style,
2079 descriptor.messages, descriptor.templates ] ) !== -1
2080 ) {
2081 // Decline to store
2082 return false;
2083 }
2084
2085 try {
2086 args = [
2087 JSON.stringify( module ),
2088 typeof descriptor.script === 'function' ?
2089 String( descriptor.script ) :
2090 JSON.stringify( descriptor.script ),
2091 JSON.stringify( descriptor.style ),
2092 JSON.stringify( descriptor.messages ),
2093 JSON.stringify( descriptor.templates )
2094 ];
2095 // Attempted workaround for a possible Opera bug (bug 57567).
2096 // This regex should never match under sane conditions.
2097 if ( /^\s*\(/.test( args[1] ) ) {
2098 args[1] = 'function' + args[1];
2099 log( 'Detected malformed function stringification (bug 57567)' );
2100 }
2101 } catch ( e ) {
2102 log( 'Storage error', e );
2103 return;
2104 }
2105
2106 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2107 mw.loader.store.update();
2108 },
2109
2110 /**
2111 * Iterate through the module store, removing any item that does not correspond
2112 * (in name and version) to an item in the module registry.
2113 */
2114 prune: function () {
2115 var key, module;
2116
2117 if ( !mw.loader.store.enabled ) {
2118 return false;
2119 }
2120
2121 for ( key in mw.loader.store.items ) {
2122 module = key.slice( 0, key.indexOf( '@' ) );
2123 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2124 mw.loader.store.stats.expired++;
2125 delete mw.loader.store.items[key];
2126 }
2127 }
2128 },
2129
2130 /**
2131 * Clear the entire module store right now.
2132 */
2133 clear: function () {
2134 mw.loader.store.items = {};
2135 localStorage.removeItem( mw.loader.store.getStoreKey() );
2136 },
2137
2138 /**
2139 * Sync modules to localStorage.
2140 *
2141 * This function debounces localStorage updates. When called multiple times in
2142 * quick succession, the calls are coalesced into a single update operation.
2143 * This allows us to call #update without having to consider the module load
2144 * queue; the call to localStorage.setItem will be naturally deferred until the
2145 * page is quiescent.
2146 *
2147 * Because localStorage is shared by all pages with the same origin, if multiple
2148 * pages are loaded with different module sets, the possibility exists that
2149 * modules saved by one page will be clobbered by another. But the impact would
2150 * be minor and the problem would be corrected by subsequent page views.
2151 *
2152 * @method
2153 */
2154 update: ( function () {
2155 var timer;
2156
2157 function flush() {
2158 var data,
2159 key = mw.loader.store.getStoreKey();
2160
2161 if ( !mw.loader.store.enabled ) {
2162 return false;
2163 }
2164 mw.loader.store.prune();
2165 try {
2166 // Replacing the content of the module store might fail if the new
2167 // contents would exceed the browser's localStorage size limit. To
2168 // avoid clogging the browser with stale data, always remove the old
2169 // value before attempting to set the new one.
2170 localStorage.removeItem( key );
2171 data = JSON.stringify( mw.loader.store );
2172 localStorage.setItem( key, data );
2173 } catch ( e ) {
2174 log( 'Storage error', e );
2175 }
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 // Alias $j to jQuery for backwards compatibility
2407 // @deprecated since 1.23 Use $ or jQuery instead
2408 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2409
2410 // Attach to window and globally alias
2411 window.mw = window.mediaWiki = mw;
2412
2413 // Auto-register from pre-loaded startup scripts
2414 if ( $.isFunction( window.startUp ) ) {
2415 window.startUp();
2416 window.startUp = undefined;
2417 }
2418
2419 }( jQuery ) );