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