5eed8b8eece120819e2d8fbd461e981d528e5517
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.js
1 /*
2 * Core MediaWiki JavaScript Library
3 */
4
5 var mw = ( function ( $, undefined ) {
6 "use strict";
7
8 /* Private Members */
9
10 var hasOwn = Object.prototype.hasOwnProperty;
11
12 /* Object constructors */
13
14 /**
15 * Map
16 *
17 * Creates an object that can be read from or written to from prototype functions
18 * that allow both single and multiple variables at once.
19 *
20 * @param global boolean Whether to store the values in the global window
21 * object or a exclusively in the object property 'values'.
22 * @return Map
23 */
24 function Map( global ) {
25 this.values = global === true ? window : {};
26 return this;
27 }
28
29 Map.prototype = {
30 /**
31 * Get the value of one or multiple a keys.
32 *
33 * If called with no arguments, all values will be returned.
34 *
35 * @param selection mixed String key or array of keys to get values for.
36 * @param fallback mixed Value to use in case key(s) do not exist (optional).
37 * @return mixed If selection was a string returns the value or null,
38 * If selection was an array, returns an object of key/values (value is null if not found),
39 * If selection was not passed or invalid, will return the 'values' object member (be careful as
40 * objects are always passed by reference in JavaScript!).
41 * @return Values as a string or object, null if invalid/inexistant.
42 */
43 get: function ( selection, fallback ) {
44 var results, i;
45
46 if ( $.isArray( selection ) ) {
47 selection = $.makeArray( selection );
48 results = {};
49 for ( i = 0; i < selection.length; i += 1 ) {
50 results[selection[i]] = this.get( selection[i], fallback );
51 }
52 return results;
53 } else if ( typeof selection === 'string' ) {
54 if ( this.values[selection] === undefined ) {
55 if ( fallback !== undefined ) {
56 return fallback;
57 }
58 return null;
59 }
60 return this.values[selection];
61 }
62 if ( selection === undefined ) {
63 return this.values;
64 } else {
65 return null; // invalid selection key
66 }
67 },
68
69 /**
70 * Sets one or multiple key/value pairs.
71 *
72 * @param selection {mixed} String key or array of keys to set values for.
73 * @param value {mixed} Value to set (optional, only in use when key is a string)
74 * @return {Boolean} This returns true on success, false on failure.
75 */
76 set: function ( selection, value ) {
77 var s;
78
79 if ( $.isPlainObject( selection ) ) {
80 for ( s in selection ) {
81 this.values[s] = selection[s];
82 }
83 return true;
84 } else if ( typeof selection === 'string' && value !== undefined ) {
85 this.values[selection] = value;
86 return true;
87 }
88 return false;
89 },
90
91 /**
92 * Checks if one or multiple keys exist.
93 *
94 * @param selection {mixed} String key or array of keys to check
95 * @return {Boolean} Existence of key(s)
96 */
97 exists: function ( selection ) {
98 var s;
99
100 if ( $.isArray( selection ) ) {
101 for ( s = 0; s < selection.length; s += 1 ) {
102 if ( this.values[selection[s]] === undefined ) {
103 return false;
104 }
105 }
106 return true;
107 } else {
108 return this.values[selection] !== undefined;
109 }
110 }
111 };
112
113 /**
114 * Message
115 *
116 * Object constructor for messages,
117 * similar to the Message class in MediaWiki PHP.
118 *
119 * @param map Map Instance of mw.Map
120 * @param key String
121 * @param parameters Array
122 * @return Message
123 */
124 function Message( map, key, parameters ) {
125 this.format = 'plain';
126 this.map = map;
127 this.key = key;
128 this.parameters = parameters === undefined ? [] : $.makeArray( parameters );
129 return this;
130 }
131
132 Message.prototype = {
133 /**
134 * Appends (does not replace) parameters for replacement to the .parameters property.
135 *
136 * @param parameters Array
137 * @return Message
138 */
139 params: function ( parameters ) {
140 var i;
141 for ( i = 0; i < parameters.length; i += 1 ) {
142 this.parameters.push( parameters[i] );
143 }
144 return this;
145 },
146
147 /**
148 * Converts message object to it's string form based on the state of format.
149 *
150 * @return string Message as a string in the current form or <key> if key does not exist.
151 */
152 toString: function() {
153 if ( !this.map.exists( this.key ) ) {
154 // Use <key> as text if key does not exist
155 if ( this.format !== 'plain' ) {
156 // format 'escape' and 'parse' need to have the brackets and key html escaped
157 return mw.html.escape( '<' + this.key + '>' );
158 }
159 return '<' + this.key + '>';
160 }
161 var text = this.map.get( this.key ),
162 parameters = this.parameters;
163
164 text = text.replace( /\$(\d+)/g, function ( str, match ) {
165 var index = parseInt( match, 10 ) - 1;
166 return parameters[index] !== undefined ? parameters[index] : '$' + match;
167 } );
168
169 if ( this.format === 'plain' ) {
170 return text;
171 }
172 if ( this.format === 'escaped' ) {
173 // According to Message.php this needs {{-transformation, which is
174 // still todo
175 return mw.html.escape( text );
176 }
177
178 /* This should be fixed up when we have a parser
179 if ( this.format === 'parse' && 'language' in mw ) {
180 text = mw.language.parse( text );
181 }
182 */
183 return text;
184 },
185
186 /**
187 * Changes format to parse and converts message to string
188 *
189 * @return {string} String form of parsed message
190 */
191 parse: function() {
192 this.format = 'parse';
193 return this.toString();
194 },
195
196 /**
197 * Changes format to plain and converts message to string
198 *
199 * @return {string} String form of plain message
200 */
201 plain: function() {
202 this.format = 'plain';
203 return this.toString();
204 },
205
206 /**
207 * Changes the format to html escaped and converts message to string
208 *
209 * @return {string} String form of html escaped message
210 */
211 escaped: function() {
212 this.format = 'escaped';
213 return this.toString();
214 },
215
216 /**
217 * Checks if message exists
218 *
219 * @return {string} String form of parsed message
220 */
221 exists: function() {
222 return this.map.exists( this.key );
223 }
224 };
225
226 return {
227 /* Public Members */
228
229 /**
230 * Dummy function which in debug mode can be replaced with a function that
231 * emulates console.log in console-less environments.
232 */
233 log: function() { },
234
235 /**
236 * @var constructor Make the Map constructor publicly available.
237 */
238 Map: Map,
239
240 /**
241 * List of configuration values
242 *
243 * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map().
244 * If $wgLegacyJavaScriptGlobals is true, this Map will have its values
245 * in the global window object.
246 */
247 config: null,
248
249 /**
250 * @var object
251 *
252 * Empty object that plugins can be installed in.
253 */
254 libs: {},
255
256 /* Extension points */
257
258 legacy: {},
259
260 /**
261 * Localization system
262 */
263 messages: new Map(),
264
265 /* Public Methods */
266
267 /**
268 * Gets a message object, similar to wfMessage()
269 *
270 * @param key string Key of message to get
271 * @param parameter_1 mixed First argument in a list of variadic arguments,
272 * each a parameter for $N replacement in messages.
273 * @return Message
274 */
275 message: function ( key, parameter_1 /* [, parameter_2] */ ) {
276 var parameters;
277 // Support variadic arguments
278 if ( parameter_1 !== undefined ) {
279 parameters = $.makeArray( arguments );
280 parameters.shift();
281 } else {
282 parameters = [];
283 }
284 return new Message( mw.messages, key, parameters );
285 },
286
287 /**
288 * Gets a message string, similar to wfMsg()
289 *
290 * @param key string Key of message to get
291 * @param parameters mixed First argument in a list of variadic arguments,
292 * each a parameter for $N replacement in messages.
293 * @return String.
294 */
295 msg: function ( key, parameters ) {
296 return mw.message.apply( mw.message, arguments ).toString();
297 },
298
299 /**
300 * Client-side module loader which integrates with the MediaWiki ResourceLoader
301 */
302 loader: ( function() {
303
304 /* Private Members */
305
306 /**
307 * Mapping of registered modules
308 *
309 * The jquery module is pre-registered, because it must have already
310 * been provided for this object to have been built, and in debug mode
311 * jquery would have been provided through a unique loader request,
312 * making it impossible to hold back registration of jquery until after
313 * mediawiki.
314 *
315 * For exact details on support for script, style and messages, look at
316 * mw.loader.implement.
317 *
318 * Format:
319 * {
320 * 'moduleName': {
321 * 'version': ############## (unix timestamp),
322 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function() {}
323 * 'group': 'somegroup', (or) null,
324 * 'source': 'local', 'someforeignwiki', (or) null
325 * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
326 * 'script': ...,
327 * 'style': ...,
328 * 'messages': { 'key': 'value' },
329 * }
330 * }
331 */
332 var registry = {},
333 /**
334 * Mapping of sources, keyed by source-id, values are objects.
335 * Format:
336 * {
337 * 'sourceId': {
338 * 'loadScript': 'http://foo.bar/w/load.php'
339 * }
340 * }
341 */
342 sources = {},
343 // List of modules which will be loaded as when ready
344 batch = [],
345 // List of modules to be loaded
346 queue = [],
347 // List of callback functions waiting for modules to be ready to be called
348 jobs = [],
349 // Flag indicating that document ready has occured
350 ready = false,
351 // Whether we should try to load scripts in a blocking way
352 // Set with setBlocking()
353 blocking = false,
354 // Selector cache for the marker element. Use getMarker() to get/use the marker!
355 $marker = null;
356
357 /* Cache document ready status */
358
359 $(document).ready( function () {
360 ready = true;
361 } );
362
363 /* Private methods */
364
365 function getMarker(){
366 // Cached ?
367 if ( $marker ) {
368 return $marker;
369 } else {
370 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
371 if ( $marker.length ) {
372 return $marker;
373 }
374 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
375 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
376 return $marker;
377 }
378 }
379
380 function compare( a, b ) {
381 var i;
382 if ( a.length !== b.length ) {
383 return false;
384 }
385 for ( i = 0; i < b.length; i += 1 ) {
386 if ( $.isArray( a[i] ) ) {
387 if ( !compare( a[i], b[i] ) ) {
388 return false;
389 }
390 }
391 if ( a[i] !== b[i] ) {
392 return false;
393 }
394 }
395 return true;
396 }
397
398 /**
399 * Generates an ISO8601 "basic" string from a UNIX timestamp
400 */
401 function formatVersionNumber( timestamp ) {
402 var pad = function ( a, b, c ) {
403 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
404 },
405 d = new Date();
406 d.setTime( timestamp * 1000 );
407 return [
408 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
409 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
410 ].join( '' );
411 }
412
413 /**
414 * Recursively resolves dependencies and detects circular references
415 */
416 function recurse( module, resolved, unresolved ) {
417 var n, deps, len;
418
419 if ( registry[module] === undefined ) {
420 throw new Error( 'Unknown dependency: ' + module );
421 }
422 // Resolves dynamic loader function and replaces it with its own results
423 if ( $.isFunction( registry[module].dependencies ) ) {
424 registry[module].dependencies = registry[module].dependencies();
425 // Ensures the module's dependencies are always in an array
426 if ( typeof registry[module].dependencies !== 'object' ) {
427 registry[module].dependencies = [registry[module].dependencies];
428 }
429 }
430 // Tracks down dependencies
431 deps = registry[module].dependencies;
432 len = deps.length;
433 for ( n = 0; n < len; n += 1 ) {
434 if ( $.inArray( deps[n], resolved ) === -1 ) {
435 if ( $.inArray( deps[n], unresolved ) !== -1 ) {
436 throw new Error(
437 'Circular reference detected: ' + module +
438 ' -> ' + deps[n]
439 );
440 }
441
442 // Add to unresolved
443 unresolved[unresolved.length] = module;
444 recurse( deps[n], resolved, unresolved );
445 // module is at the end of unresolved
446 unresolved.pop();
447 }
448 }
449 resolved[resolved.length] = module;
450 }
451
452 /**
453 * Gets a list of module names that a module depends on in their proper dependency order
454 *
455 * @param module string module name or array of string module names
456 * @return list of dependencies, including 'module'.
457 * @throws Error if circular reference is detected
458 */
459 function resolve( module ) {
460 var modules, m, deps, n, resolved;
461
462 // Allow calling with an array of module names
463 if ( $.isArray( module ) ) {
464 modules = [];
465 for ( m = 0; m < module.length; m += 1 ) {
466 deps = resolve( module[m] );
467 for ( n = 0; n < deps.length; n += 1 ) {
468 modules[modules.length] = deps[n];
469 }
470 }
471 return modules;
472 } else if ( typeof module === 'string' ) {
473 resolved = [];
474 recurse( module, resolved, [] );
475 return resolved;
476 }
477 throw new Error( 'Invalid module argument: ' + module );
478 }
479
480 /**
481 * Narrows a list of module names down to those matching a specific
482 * state (see comment on top of this scope for a list of valid states).
483 * One can also filter for 'unregistered', which will return the
484 * modules names that don't have a registry entry.
485 *
486 * @param states string or array of strings of module states to filter by
487 * @param modules array list of module names to filter (optional, by default the entire
488 * registry is used)
489 * @return array list of filtered module names
490 */
491 function filter( states, modules ) {
492 var list, module, s, m;
493
494 // Allow states to be given as a string
495 if ( typeof states === 'string' ) {
496 states = [states];
497 }
498 // If called without a list of modules, build and use a list of all modules
499 list = [];
500 if ( modules === undefined ) {
501 modules = [];
502 for ( module in registry ) {
503 modules[modules.length] = module;
504 }
505 }
506 // Build a list of modules which are in one of the specified states
507 for ( s = 0; s < states.length; s += 1 ) {
508 for ( m = 0; m < modules.length; m += 1 ) {
509 if ( registry[modules[m]] === undefined ) {
510 // Module does not exist
511 if ( states[s] === 'unregistered' ) {
512 // OK, undefined
513 list[list.length] = modules[m];
514 }
515 } else {
516 // Module exists, check state
517 if ( registry[modules[m]].state === states[s] ) {
518 // OK, correct state
519 list[list.length] = modules[m];
520 }
521 }
522 }
523 }
524 return list;
525 }
526
527 /**
528 * Automatically executes jobs and modules which are pending with satistifed dependencies.
529 *
530 * This is used when dependencies are satisfied, such as when a module is executed.
531 */
532 function handlePending( module ) {
533 var j, r;
534
535 try {
536 // Run jobs who's dependencies have just been met
537 for ( j = 0; j < jobs.length; j += 1 ) {
538 if ( compare(
539 filter( 'ready', jobs[j].dependencies ),
540 jobs[j].dependencies ) )
541 {
542 if ( $.isFunction( jobs[j].ready ) ) {
543 jobs[j].ready();
544 }
545 jobs.splice( j, 1 );
546 j -= 1;
547 }
548 }
549 // Execute modules who's dependencies have just been met
550 for ( r in registry ) {
551 if ( registry[r].state === 'loaded' ) {
552 if ( compare(
553 filter( ['ready'], registry[r].dependencies ),
554 registry[r].dependencies ) )
555 {
556 execute( r );
557 }
558 }
559 }
560 } catch ( e ) {
561 // Run error callbacks of jobs affected by this condition
562 for ( j = 0; j < jobs.length; j += 1 ) {
563 if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) {
564 if ( $.isFunction( jobs[j].error ) ) {
565 jobs[j].error( e, module );
566 }
567 jobs.splice( j, 1 );
568 j -= 1;
569 }
570 }
571 }
572 }
573
574 /**
575 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
576 * depending on whether document-ready has occured yet and whether we are in blocking mode.
577 *
578 * @param src String: URL to script, will be used as the src attribute in the script tag
579 * @param callback Function: Optional callback which will be run when the script is done
580 */
581 function addScript( src, callback ) {
582 var done = false, script, head;
583 if ( ready || !blocking ) {
584 // jQuery's getScript method is NOT better than doing this the old-fashioned way
585 // because jQuery will eval the script's code, and errors will not have sane
586 // line numbers.
587 script = document.createElement( 'script' );
588 script.setAttribute( 'src', src );
589 script.setAttribute( 'type', 'text/javascript' );
590 if ( $.isFunction( callback ) ) {
591 // Attach handlers for all browsers (based on jQuery.ajax)
592 script.onload = script.onreadystatechange = function() {
593
594 if (
595 !done
596 && (
597 !script.readyState
598 || /loaded|complete/.test( script.readyState )
599 )
600 ) {
601
602 done = true;
603
604 // Handle memory leak in IE
605 script.onload = script.onreadystatechange = null;
606
607 callback();
608
609 if ( script.parentNode ) {
610 script.parentNode.removeChild( script );
611 }
612
613 // Dereference the script
614 script = undefined;
615 }
616 };
617 }
618 // IE-safe way of getting the <head> . document.documentElement.head doesn't
619 // work in scripts that run in the <head>
620 head = document.getElementsByTagName( 'head' )[0];
621 // Append to the <body> if available, to the <head> otherwise
622 (document.body || head).appendChild( script );
623 } else {
624 document.write( mw.html.element(
625 'script', { 'type': 'text/javascript', 'src': src }, ''
626 ) );
627 if ( $.isFunction( callback ) ) {
628 // Document.write is synchronous, so this is called when it's done
629 // FIXME: that's a lie. doc.write isn't actually synchronous
630 callback();
631 }
632 }
633 }
634
635 /**
636 * Executes a loaded module, making it ready to use
637 *
638 * @param module string module name to execute
639 */
640 function execute( module, callback ) {
641 var style, media, i, script, markModuleReady, nestedAddScript;
642
643 if ( registry[module] === undefined ) {
644 throw new Error( 'Module has not been registered yet: ' + module );
645 } else if ( registry[module].state === 'registered' ) {
646 throw new Error( 'Module has not been requested from the server yet: ' + module );
647 } else if ( registry[module].state === 'loading' ) {
648 throw new Error( 'Module has not completed loading yet: ' + module );
649 } else if ( registry[module].state === 'ready' ) {
650 throw new Error( 'Module has already been loaded: ' + module );
651 }
652
653 // Add styles
654 if ( $.isPlainObject( registry[module].style ) ) {
655 for ( media in registry[module].style ) {
656 style = registry[module].style[media];
657 if ( $.isArray( style ) ) {
658 for ( i = 0; i < style.length; i += 1 ) {
659 getMarker().before( mw.html.element( 'link', {
660 'type': 'text/css',
661 'media': media,
662 'rel': 'stylesheet',
663 'href': style[i]
664 } ) );
665 }
666 } else if ( typeof style === 'string' ) {
667 getMarker().before( mw.html.element( 'style', {
668 'type': 'text/css',
669 'media': media
670 }, new mw.html.Cdata( style ) ) );
671 }
672 }
673 }
674 // Add localizations to message system
675 if ( $.isPlainObject( registry[module].messages ) ) {
676 mw.messages.set( registry[module].messages );
677 }
678 // Execute script
679 try {
680 script = registry[module].script;
681 markModuleReady = function() {
682 registry[module].state = 'ready';
683 handlePending( module );
684 if ( $.isFunction( callback ) ) {
685 callback();
686 }
687 };
688 nestedAddScript = function ( arr, callback, i ) {
689 // Recursively call addScript() in its own callback
690 // for each element of arr.
691 if ( i >= arr.length ) {
692 // We're at the end of the array
693 callback();
694 return;
695 }
696
697 addScript( arr[i], function() {
698 nestedAddScript( arr, callback, i + 1 );
699 } );
700 };
701
702 if ( $.isArray( script ) ) {
703 registry[module].state = 'loading';
704 nestedAddScript( script, markModuleReady, 0 );
705 } else if ( $.isFunction( script ) ) {
706 script( $ );
707 markModuleReady();
708 }
709 } catch ( e ) {
710 // This needs to NOT use mw.log because these errors are common in production mode
711 // and not in debug mode, such as when a symbol that should be global isn't exported
712 if ( window.console && typeof window.console.log === 'function' ) {
713 console.log( 'mw.loader::execute> Exception thrown by ' + module + ': ' + e.message );
714 }
715 registry[module].state = 'error';
716 throw e;
717 }
718 }
719
720 /**
721 * Adds a dependencies to the queue with optional callbacks to be run
722 * when the dependencies are ready or fail
723 *
724 * @param dependencies string module name or array of string module names
725 * @param ready function callback to execute when all dependencies are ready
726 * @param error function callback to execute when any dependency fails
727 */
728 function request( dependencies, ready, error ) {
729 var regItemDeps, regItemDepLen, n;
730
731 // Allow calling by single module name
732 if ( typeof dependencies === 'string' ) {
733 dependencies = [dependencies];
734 if ( registry[dependencies[0]] !== undefined ) {
735 // Cache repetitively accessed deep level object member
736 regItemDeps = registry[dependencies[0]].dependencies;
737 // Cache to avoid looped access to length property
738 regItemDepLen = regItemDeps.length;
739 for ( n = 0; n < regItemDepLen; n += 1 ) {
740 dependencies[dependencies.length] = regItemDeps[n];
741 }
742 }
743 }
744 // Add ready and error callbacks if they were given
745 if ( arguments.length > 1 ) {
746 jobs[jobs.length] = {
747 'dependencies': filter(
748 ['registered', 'loading', 'loaded'],
749 dependencies
750 ),
751 'ready': ready,
752 'error': error
753 };
754 }
755 // Queue up any dependencies that are registered
756 dependencies = filter( ['registered'], dependencies );
757 for ( n = 0; n < dependencies.length; n += 1 ) {
758 if ( $.inArray( dependencies[n], queue ) === -1 ) {
759 queue[queue.length] = dependencies[n];
760 }
761 }
762 // Work the queue
763 mw.loader.work();
764 }
765
766 function sortQuery(o) {
767 var sorted = {}, key, a = [];
768 for ( key in o ) {
769 if ( hasOwn.call( o, key ) ) {
770 a.push( key );
771 }
772 }
773 a.sort();
774 for ( key = 0; key < a.length; key += 1 ) {
775 sorted[a[key]] = o[a[key]];
776 }
777 return sorted;
778 }
779
780 /**
781 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
782 * to a query string of the form foo.bar,baz|bar.baz,quux
783 */
784 function buildModulesString( moduleMap ) {
785 var arr = [], p, prefix;
786 for ( prefix in moduleMap ) {
787 p = prefix === '' ? '' : prefix + '.';
788 arr.push( p + moduleMap[prefix].join( ',' ) );
789 }
790 return arr.join( '|' );
791 }
792
793 /**
794 * Asynchronously append a script tag to the end of the body
795 * that invokes load.php
796 * @param moduleMap {Object}: Module map, see buildModulesString()
797 * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request
798 * @param sourceLoadScript {String}: URL of load.php
799 */
800 function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
801 var request = $.extend(
802 { 'modules': buildModulesString( moduleMap ) },
803 currReqBase
804 );
805 request = sortQuery( request );
806 // Asynchronously append a script tag to the end of the body
807 // Append &* to avoid triggering the IE6 extension check
808 addScript( sourceLoadScript + '?' + $.param( request ) + '&*' );
809 }
810
811 /* Public Methods */
812 return {
813 /**
814 * Requests dependencies from server, loading and executing when things when ready.
815 */
816 work: function () {
817 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
818 source, group, g, i, modules, maxVersion, sourceLoadScript,
819 currReqBase, currReqBaseLength, moduleMap, l,
820 lastDotIndex, prefix, suffix, bytesAdded;
821
822 // Build a list of request parameters common to all requests.
823 reqBase = {
824 skin: mw.config.get( 'skin' ),
825 lang: mw.config.get( 'wgUserLanguage' ),
826 debug: mw.config.get( 'debug' )
827 };
828 // Split module batch by source and by group.
829 splits = {};
830 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
831
832 // Appends a list of modules from the queue to the batch
833 for ( q = 0; q < queue.length; q += 1 ) {
834 // Only request modules which are registered
835 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
836 // Prevent duplicate entries
837 if ( $.inArray( queue[q], batch ) === -1 ) {
838 batch[batch.length] = queue[q];
839 // Mark registered modules as loading
840 registry[queue[q]].state = 'loading';
841 }
842 }
843 }
844 // Early exit if there's nothing to load...
845 if ( !batch.length ) {
846 return;
847 }
848
849 // The queue has been processed into the batch, clear up the queue.
850 queue = [];
851
852 // Always order modules alphabetically to help reduce cache
853 // misses for otherwise identical content.
854 batch.sort();
855
856 // Split batch by source and by group.
857 for ( b = 0; b < batch.length; b += 1 ) {
858 bSource = registry[batch[b]].source;
859 bGroup = registry[batch[b]].group;
860 if ( splits[bSource] === undefined ) {
861 splits[bSource] = {};
862 }
863 if ( splits[bSource][bGroup] === undefined ) {
864 splits[bSource][bGroup] = [];
865 }
866 bSourceGroup = splits[bSource][bGroup];
867 bSourceGroup[bSourceGroup.length] = batch[b];
868 }
869
870 // Clear the batch - this MUST happen before we append any
871 // script elements to the body or it's possible that a script
872 // will be locally cached, instantly load, and work the batch
873 // again, all before we've cleared it causing each request to
874 // include modules which are already loaded.
875 batch = [];
876
877 for ( source in splits ) {
878
879 sourceLoadScript = sources[source].loadScript;
880
881 for ( group in splits[source] ) {
882
883 // Cache access to currently selected list of
884 // modules for this group from this source.
885 modules = splits[source][group];
886
887 // Calculate the highest timestamp
888 maxVersion = 0;
889 for ( g = 0; g < modules.length; g += 1 ) {
890 if ( registry[modules[g]].version > maxVersion ) {
891 maxVersion = registry[modules[g]].version;
892 }
893 }
894
895 currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase );
896 currReqBaseLength = $.param( currReqBase ).length;
897 moduleMap = {};
898 // We may need to split up the request to honor the query string length limit,
899 // so build it piece by piece.
900 l = currReqBaseLength + 9; // '&modules='.length == 9
901
902 moduleMap = {}; // { prefix: [ suffixes ] }
903
904 for ( i = 0; i < modules.length; i += 1 ) {
905 // Determine how many bytes this module would add to the query string
906 lastDotIndex = modules[i].lastIndexOf( '.' );
907 // Note that these substr() calls work even if lastDotIndex == -1
908 prefix = modules[i].substr( 0, lastDotIndex );
909 suffix = modules[i].substr( lastDotIndex + 1 );
910 bytesAdded = moduleMap[prefix] !== undefined
911 ? suffix.length + 3 // '%2C'.length == 3
912 : modules[i].length + 3; // '%7C'.length == 3
913
914 // If the request would become too long, create a new one,
915 // but don't create empty requests
916 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
917 // This request would become too long, create a new one
918 // and fire off the old one
919 doRequest( moduleMap, currReqBase, sourceLoadScript );
920 moduleMap = {};
921 l = currReqBaseLength + 9;
922 }
923 if ( moduleMap[prefix] === undefined ) {
924 moduleMap[prefix] = [];
925 }
926 moduleMap[prefix].push( suffix );
927 l += bytesAdded;
928 }
929 // If there's anything left in moduleMap, request that too
930 if ( !$.isEmptyObject( moduleMap ) ) {
931 doRequest( moduleMap, currReqBase, sourceLoadScript );
932 }
933 }
934 }
935 },
936
937 /**
938 * Register a source.
939 *
940 * @param id {String}: Short lowercase a-Z string representing a source, only used internally.
941 * @param props {Object}: Object containing only the loadScript property which is a url to
942 * the load.php location of the source.
943 * @return {Boolean}
944 */
945 addSource: function ( id, props ) {
946 var source;
947 // Allow multiple additions
948 if ( typeof id === 'object' ) {
949 for ( source in id ) {
950 mw.loader.addSource( source, id[source] );
951 }
952 return true;
953 }
954
955 if ( sources[id] !== undefined ) {
956 throw new Error( 'source already registered: ' + id );
957 }
958
959 sources[id] = props;
960
961 return true;
962 },
963
964 /**
965 * Registers a module, letting the system know about it and its
966 * properties. Startup modules contain calls to this function.
967 *
968 * @param module {String}: Module name
969 * @param version {Number}: Module version number as a timestamp (falls backs to 0)
970 * @param dependencies {String|Array|Function}: One string or array of strings of module
971 * names on which this module depends, or a function that returns that array.
972 * @param group {String}: Group which the module is in (optional, defaults to null)
973 * @param source {String}: Name of the source. Defaults to local.
974 */
975 register: function ( module, version, dependencies, group, source ) {
976 var m;
977 // Allow multiple registration
978 if ( typeof module === 'object' ) {
979 for ( m = 0; m < module.length; m += 1 ) {
980 // module is an array of module names
981 if ( typeof module[m] === 'string' ) {
982 mw.loader.register( module[m] );
983 // module is an array of arrays
984 } else if ( typeof module[m] === 'object' ) {
985 mw.loader.register.apply( mw.loader, module[m] );
986 }
987 }
988 return;
989 }
990 // Validate input
991 if ( typeof module !== 'string' ) {
992 throw new Error( 'module must be a string, not a ' + typeof module );
993 }
994 if ( registry[module] !== undefined ) {
995 throw new Error( 'module already registered: ' + module );
996 }
997 // List the module as registered
998 registry[module] = {
999 'version': version !== undefined ? parseInt( version, 10 ) : 0,
1000 'dependencies': [],
1001 'group': typeof group === 'string' ? group : null,
1002 'source': typeof source === 'string' ? source: 'local',
1003 'state': 'registered'
1004 };
1005 if ( typeof dependencies === 'string' ) {
1006 // Allow dependencies to be given as a single module name
1007 registry[module].dependencies = [dependencies];
1008 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1009 // Allow dependencies to be given as an array of module names
1010 // or a function which returns an array
1011 registry[module].dependencies = dependencies;
1012 }
1013 },
1014
1015 /**
1016 * Implements a module, giving the system a course of action to take
1017 * upon loading. Results of a request for one or more modules contain
1018 * calls to this function.
1019 *
1020 * All arguments are required.
1021 *
1022 * @param module String: Name of module
1023 * @param script Mixed: Function of module code or String of URL to be used as the src
1024 * attribute when adding a script element to the body
1025 * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs
1026 * keyed by media-type
1027 * @param msgs Object: List of key/value pairs to be passed through mw.messages.set
1028 */
1029 implement: function ( module, script, style, msgs ) {
1030 // Validate input
1031 if ( typeof module !== 'string' ) {
1032 throw new Error( 'module must be a string, not a ' + typeof module );
1033 }
1034 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1035 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1036 }
1037 if ( !$.isPlainObject( style ) ) {
1038 throw new Error( 'style must be an object, not a ' + typeof style );
1039 }
1040 if ( !$.isPlainObject( msgs ) ) {
1041 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1042 }
1043 // Automatically register module
1044 if ( registry[module] === undefined ) {
1045 mw.loader.register( module );
1046 }
1047 // Check for duplicate implementation
1048 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1049 throw new Error( 'module already implemented: ' + module );
1050 }
1051 // Mark module as loaded
1052 registry[module].state = 'loaded';
1053 // Attach components
1054 registry[module].script = script;
1055 registry[module].style = style;
1056 registry[module].messages = msgs;
1057 // Execute or queue callback
1058 if ( compare(
1059 filter( ['ready'], registry[module].dependencies ),
1060 registry[module].dependencies ) )
1061 {
1062 execute( module );
1063 }
1064 },
1065
1066 /**
1067 * Executes a function as soon as one or more required modules are ready
1068 *
1069 * @param dependencies {String|Array} Module name or array of modules names the callback
1070 * dependends on to be ready before executing
1071 * @param ready {Function} callback to execute when all dependencies are ready (optional)
1072 * @param error {Function} callback to execute when if dependencies have a errors (optional)
1073 */
1074 using: function ( dependencies, ready, error ) {
1075 var tod = typeof dependencies;
1076 // Validate input
1077 if ( tod !== 'object' && tod !== 'string' ) {
1078 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1079 }
1080 // Allow calling with a single dependency as a string
1081 if ( tod === 'string' ) {
1082 dependencies = [dependencies];
1083 }
1084 // Resolve entire dependency map
1085 dependencies = resolve( dependencies );
1086 // If all dependencies are met, execute ready immediately
1087 if ( compare( filter( ['ready'], dependencies ), dependencies ) ) {
1088 if ( $.isFunction( ready ) ) {
1089 ready();
1090 }
1091 }
1092 // If any dependencies have errors execute error immediately
1093 else if ( filter( ['error'], dependencies ).length ) {
1094 if ( $.isFunction( error ) ) {
1095 error( new Error( 'one or more dependencies have state "error"' ),
1096 dependencies );
1097 }
1098 }
1099 // Since some dependencies are not yet ready, queue up a request
1100 else {
1101 request( dependencies, ready, error );
1102 }
1103 },
1104
1105 /**
1106 * Loads an external script or one or more modules for future use
1107 *
1108 * @param modules {mixed} Either the name of a module, array of modules,
1109 * or a URL of an external script or style
1110 * @param type {String} mime-type to use if calling with a URL of an
1111 * external script or style; acceptable values are "text/css" and
1112 * "text/javascript"; if no type is provided, text/javascript is assumed.
1113 */
1114 load: function ( modules, type ) {
1115 var filtered, m;
1116
1117 // Validate input
1118 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1119 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1120 }
1121 // Allow calling with an external url or single dependency as a string
1122 if ( typeof modules === 'string' ) {
1123 // Support adding arbitrary external scripts
1124 if ( /^(https?:)?\/\//.test( modules ) ) {
1125 if ( type === 'text/css' ) {
1126 $( 'head' ).append( $( '<link>', {
1127 rel: 'stylesheet',
1128 type: 'text/css',
1129 href: modules
1130 } ) );
1131 return;
1132 } else if ( type === 'text/javascript' || type === undefined ) {
1133 addScript( modules );
1134 return;
1135 }
1136 // Unknown type
1137 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1138 }
1139 // Called with single module
1140 modules = [modules];
1141 }
1142
1143 // Filter out undefined modules, otherwise resolve() will throw
1144 // an exception for trying to load an undefined module.
1145 // Undefined modules are acceptable here in load(), because load() takes
1146 // an array of unrelated modules, whereas the modules passed to
1147 // using() are related and must all be loaded.
1148 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1149 if ( registry[modules[m]] !== undefined ) {
1150 filtered[filtered.length] = modules[m];
1151 }
1152 }
1153
1154 // Resolve entire dependency map
1155 filtered = resolve( filtered );
1156 // If all modules are ready, nothing dependency be done
1157 if ( compare( filter( ['ready'], filtered ), filtered ) ) {
1158 return;
1159 }
1160 // If any modules have errors
1161 else if ( filter( ['error'], filtered ).length ) {
1162 return;
1163 }
1164 // Since some modules are not yet ready, queue up a request
1165 else {
1166 request( filtered );
1167 return;
1168 }
1169 },
1170
1171 /**
1172 * Changes the state of a module
1173 *
1174 * @param module {String|Object} module name or object of module name/state pairs
1175 * @param state {String} state name
1176 */
1177 state: function ( module, state ) {
1178 var m;
1179 if ( typeof module === 'object' ) {
1180 for ( m in module ) {
1181 mw.loader.state( m, module[m] );
1182 }
1183 return;
1184 }
1185 if ( registry[module] === undefined ) {
1186 mw.loader.register( module );
1187 }
1188 registry[module].state = state;
1189 },
1190
1191 /**
1192 * Gets the version of a module
1193 *
1194 * @param module string name of module to get version for
1195 */
1196 getVersion: function ( module ) {
1197 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1198 return formatVersionNumber( registry[module].version );
1199 }
1200 return null;
1201 },
1202
1203 /**
1204 * @deprecated since 1.18 use mw.loader.getVersion() instead
1205 */
1206 version: function () {
1207 return mw.loader.getVersion.apply( mw.loader, arguments );
1208 },
1209
1210 /**
1211 * Gets the state of a module
1212 *
1213 * @param module string name of module to get state for
1214 */
1215 getState: function ( module ) {
1216 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1217 return registry[module].state;
1218 }
1219 return null;
1220 },
1221
1222 /**
1223 * Get names of all registered modules.
1224 *
1225 * @return {Array}
1226 */
1227 getModuleNames: function () {
1228 return $.map( registry, function ( i, key ) {
1229 return key;
1230 } );
1231 },
1232
1233 /**
1234 * Enable or disable blocking. If blocking is enabled and
1235 * document ready has not yet occurred, scripts will be loaded
1236 * in a blocking way (using document.write) rather than
1237 * asynchronously using DOM manipulation
1238 *
1239 * @param b {Boolean} True to enable blocking, false to disable it
1240 */
1241 setBlocking: function( b ) {
1242 blocking = b;
1243 },
1244
1245 /**
1246 * For backwards-compatibility with Squid-cached pages. Loads mw.user
1247 */
1248 go: function () {
1249 mw.loader.load( 'mediawiki.user' );
1250 }
1251 };
1252 }() ),
1253
1254 /** HTML construction helper functions */
1255 html: ( function () {
1256 function escapeCallback( s ) {
1257 switch ( s ) {
1258 case "'":
1259 return '&#039;';
1260 case '"':
1261 return '&quot;';
1262 case '<':
1263 return '&lt;';
1264 case '>':
1265 return '&gt;';
1266 case '&':
1267 return '&amp;';
1268 }
1269 }
1270
1271 return {
1272 /**
1273 * Escape a string for HTML. Converts special characters to HTML entities.
1274 * @param s The string to escape
1275 */
1276 escape: function ( s ) {
1277 return s.replace( /['"<>&]/g, escapeCallback );
1278 },
1279
1280 /**
1281 * Wrapper object for raw HTML passed to mw.html.element().
1282 * @constructor
1283 */
1284 Raw: function ( value ) {
1285 this.value = value;
1286 },
1287
1288 /**
1289 * Wrapper object for CDATA element contents passed to mw.html.element()
1290 * @constructor
1291 */
1292 Cdata: function ( value ) {
1293 this.value = value;
1294 },
1295
1296 /**
1297 * Create an HTML element string, with safe escaping.
1298 *
1299 * @param name The tag name.
1300 * @param attrs An object with members mapping element names to values
1301 * @param contents The contents of the element. May be either:
1302 * - string: The string is escaped.
1303 * - null or undefined: The short closing form is used, e.g. <br/>.
1304 * - this.Raw: The value attribute is included without escaping.
1305 * - this.Cdata: The value attribute is included, and an exception is
1306 * thrown if it contains an illegal ETAGO delimiter.
1307 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1308 *
1309 * Example:
1310 * var h = mw.html;
1311 * return h.element( 'div', {},
1312 * new h.Raw( h.element( 'img', {src: '<'} ) ) );
1313 * Returns <div><img src="&lt;"/></div>
1314 */
1315 element: function ( name, attrs, contents ) {
1316 var v, attrName, s = '<' + name;
1317
1318 for ( attrName in attrs ) {
1319 v = attrs[attrName];
1320 // Convert name=true, to name=name
1321 if ( v === true ) {
1322 v = attrName;
1323 // Skip name=false
1324 } else if ( v === false ) {
1325 continue;
1326 }
1327 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1328 }
1329 if ( contents === undefined || contents === null ) {
1330 // Self close tag
1331 s += '/>';
1332 return s;
1333 }
1334 // Regular open tag
1335 s += '>';
1336 switch ( typeof contents ) {
1337 case 'string':
1338 // Escaped
1339 s += this.escape( contents );
1340 break;
1341 case 'number':
1342 case 'boolean':
1343 // Convert to string
1344 s += String( contents );
1345 break;
1346 default:
1347 if ( contents instanceof this.Raw ) {
1348 // Raw HTML inclusion
1349 s += contents.value;
1350 } else if ( contents instanceof this.Cdata ) {
1351 // CDATA
1352 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1353 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1354 }
1355 s += contents.value;
1356 } else {
1357 throw new Error( 'mw.html.element: Invalid type of contents' );
1358 }
1359 }
1360 s += '</' + name + '>';
1361 return s;
1362 }
1363 };
1364 })()
1365 };
1366
1367 })( jQuery );
1368
1369 // Alias $j to jQuery for backwards compatibility
1370 window.$j = jQuery;
1371
1372 // Attach to window and globally alias
1373 window.mw = window.mediaWiki = mw;
1374
1375 // Auto-register from pre-loaded startup scripts
1376 if ( typeof startUp !== 'undefined' && jQuery.isFunction( startUp ) ) {
1377 startUp();
1378 startUp = undefined;
1379 }