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