Use of undefined constant h in line 250. Follow up to r81558
[mediawiki.git] / resources / mediawiki / mediawiki.js
blob90c803288b7da4a0ef87bd729b4e107bba08d8da
1 /*
2  * JavaScript backwards-compatibility alternatives and other convenience functions
3  */
5 jQuery.extend({
6         trimLeft : function( str ) {
7                 return str == null ? '' : str.toString().replace( /^\s+/, '' );
8         },
9         trimRight : function( str ) {
10                 return str == null ?
11                                 '' : str.toString().replace( /\s+$/, '' );
12         },
13         ucFirst : function( str ) {
14                 return str.substr( 0, 1 ).toUpperCase() + str.substr( 1, str.length );
15         },
16         escapeRE : function( str ) {
17                 return str.replace ( /([\\{}()|.?*+^$\[\]])/g, "\\$1" );
18         },
19         isEmpty : function( v ) {
20                 var key;
21                 if ( v === "" || v === 0 || v === "0" || v === null
22                         || v === false || typeof v === 'undefined' )
23                 {
24                         return true;
25                 }
26                 // the for-loop could potentially contain prototypes
27                 // to avoid that we check it's length first
28                 if ( v.length === 0 ) {
29                         return true;
30                 }
31                 if ( typeof v === 'object' ) {
32                         for ( key in v ) {
33                                 return false;
34                         }
35                         return true;
36                 }
37                 return false;
38         },
39         compareArray : function( arrThis, arrAgainst ) {
40                 if ( arrThis.length != arrAgainst.length ) {
41                         return false;
42                 }
43                 for ( var i = 0; i < arrThis.length; i++ ) {
44                         if ( arrThis[i] instanceof Array ) {
45                                 if ( !$.compareArray( arrThis[i], arrAgainst[i] ) ) {
46                                         return false;
47                                 }
48                         } else if ( arrThis[i] !== arrAgainst[i] ) {
49                                 return false;
50                         }
51                 }
52                 return true;
53         },
54         compareObject : function( objectA, objectB ) {
55         
56                 // Do a simple check if the types match
57                 if ( typeof( objectA ) == typeof( objectB ) ) {
58         
59                         // Only loop over the contents if it really is an object
60                         if ( typeof( objectA ) == 'object' ) {
61                                 // If they are aliases of the same object (ie. mw and mediaWiki) return now
62                                 if ( objectA === objectB ) {
63                                         return true;
64                                 } else {
65                                         // Iterate over each property
66                                         for ( var prop in objectA ) {
67                                                 // Check if this property is also present in the other object
68                                                 if ( prop in objectB ) {
69                                                         // Compare the types of the properties
70                                                         var type = typeof( objectA[prop] );
71                                                         if ( type == typeof( objectB[prop] ) ) {
72                                                                 // Recursively check objects inside this one
73                                                                 switch ( type ) {
74                                                                         case 'object' :
75                                                                                 if ( !$.compareObject( objectA[prop], objectB[prop] ) ) {
76                                                                                         return false;
77                                                                                 }
78                                                                                 break;
79                                                                         case 'function' :
80                                                                                 // Functions need to be strings to compare them properly
81                                                                                 if ( objectA[prop].toString() !== objectB[prop].toString() ) {
82                                                                                         return false;
83                                                                                 }
84                                                                                 break;
85                                                                         default:
86                                                                                 // Strings, numbers
87                                                                                 if ( objectA[prop] !== objectB[prop] ) {
88                                                                                         return false;
89                                                                                 }
90                                                                                 break;
91                                                                 }
92                                                         } else {
93                                                                 return false;
94                                                         }
95                                                 } else {
96                                                         return false;
97                                                 }
98                                         }
99                                         // Check for properties in B but not in A
100                                         // This is about 15% faster (tested in Safari 5 and Firefox 3.6)
101                                         // ...than incrementing a count variable in the above and below loops
102                                         // See also: http://www.mediawiki.org/wiki/ResourceLoader/Default_modules/compareObject_test#Results
103                                         for ( var prop in objectB ) {
104                                                 if ( !( prop in objectA ) ) {
105                                                         return false;
106                                                 }
107                                         }
108                                 }
109                         }
110                 } else {
111                         return false;
112                 }
113                 return true;
114         }
118  * Core MediaWiki JavaScript Library
119  */
121 // Attach to window
122 window.mediaWiki = new ( function( $ ) {
124         /* Constants */
126         // This will not change until we are 100% ready to turn off legacy globals
127         var LEGACY_GLOBALS = true;
129         /* Private Members */
131         // List of messages that have been requested to be loaded
132         var messageQueue = {};
134         /* Prototypes */
136         /**
137          * An object which allows single and multiple get/set/exists functionality
138          * on a list of key / value pairs.
139          *
140          * @param {boolean} global Whether to get/set/exists values on the window
141          *   object or a private object
142          */
143         function Map( global ) {
144                 this.values = ( global === true ) ? window : {};
145         }
147         /**
148          * Gets the value of a key, or a list of key/value pairs for an array of keys.
149          *
150          * If called with no arguments, all values will be returned.
151          *
152          * @param selection mixed Key or array of keys to get values for
153          * @param fallback mixed Value to use in case key(s) do not exist (optional)
154          */
155         Map.prototype.get = function( selection, fallback ) {
156                 if ( typeof selection === 'object' ) {
157                         selection = $.makeArray( selection );
158                         var results = {};
159                         for ( var i = 0; i < selection.length; i++ ) {
160                                 results[selection[i]] = this.get( selection[i], fallback );
161                         }
162                         return results;
163                 } else if ( typeof selection === 'string' ) {
164                         if ( typeof this.values[selection] === 'undefined' ) {
165                                 if ( typeof fallback !== 'undefined' ) {
166                                         return fallback;
167                                 }
168                                 return null;
169                         }
170                         return this.values[selection];
171                 }
172                 return this.values;
173         };
175         /**
176          * Sets one or multiple key/value pairs.
177          *
178          * @param selection mixed Key or object of key/value pairs to set
179          * @param value mixed Value to set (optional, only in use when key is a string)
180          */
181         Map.prototype.set = function( selection, value ) {
182                 if ( typeof selection === 'object' ) {
183                         for ( var s in selection ) {
184                                 this.values[s] = selection[s];
185                         }
186                 } else if ( typeof selection === 'string' && typeof value !== 'undefined' ) {
187                         this.values[selection] = value;
188                 }
189         };
191         /**
192          * Checks if one or multiple keys exist.
193          *
194          * @param selection mixed Key or array of keys to check
195          * @return boolean Existence of key(s)
196          */
197         Map.prototype.exists = function( selection ) {
198                 if ( typeof selection === 'object' ) {
199                         for ( var s = 0; s < selection.length; s++ ) {
200                                 if ( !( selection[s] in this.values ) ) {
201                                         return false;
202                                 }
203                         }
204                         return true;
205                 } else {
206                         return selection in this.values;
207                 }
208         };
210         /**
211          * Message object, similar to Message in PHP
212          */
213         function Message( map, key, parameters ) {
214                 this.format = 'parse';
215                 this.map = map;
216                 this.key = key;
217                 this.parameters = typeof parameters === 'undefined' ? [] : $.makeArray( parameters );
218         }
220         /**
221          * Appends parameters for replacement
222          *
223          * @param parameters mixed First in a list of variadic arguments to append as message parameters
224          */
225         Message.prototype.params = function( parameters ) {
226                 for ( var i = 0; i < parameters.length; i++ ) {
227                         this.parameters[this.parameters.length] = parameters[i];
228                 }
229                 return this;
230         };
232         /**
233          * Converts message object to it's string form based on the state of format
234          *
235          * @return {string} String form of message
236          */
237         Message.prototype.toString = function() {
238                 if ( !this.map.exists( this.key ) ) {
239                         // Return <key> if key does not exist
240                         return '<' + this.key + '>';
241                 }
242                 var text = this.map.get( this.key );
243                 var parameters = this.parameters;
244                 text = text.replace( /\$(\d+)/g, function( string, match ) {
245                         var index = parseInt( match, 10 ) - 1;
246                         return index in parameters ? parameters[index] : '$' + match;
247                 } );
248                 /* This should be fixed up when we have a parser
249                 if ( this.format === 'parse' && 'language' in mediaWiki ) {
250                         text = mediaWiki.language.parse( text );
251                 }
252                 */
253                 return text;
254         };
256         /**
257          * Changes format to parse and converts message to string
258          *
259          * @return {string} String form of parsed message
260          */
261         Message.prototype.parse = function() {
262                 this.format = 'parse';
263                 return this.toString();
264         };
266         /**
267          * Changes format to plain and converts message to string
268          *
269          * @return {string} String form of plain message
270          */
271         Message.prototype.plain = function() {
272                 this.format = 'plain';
273                 return this.toString();
274         };
276         /**
277          * Checks if message exists
278          *
279          * @return {string} String form of parsed message
280          */
281         Message.prototype.exists = function() {
282                 return this.map.exists( this.key );
283         };
285         /**
286          * User object
287          */
288         function User() {
290                 /* Private Members */
292                 var that = this;
294                 /* Public Members */
296                 this.options = new Map();
298                 /* Public Methods */
300                 /**
301                  * Generates a random user session ID (32 alpha-numeric characters).
302                  * 
303                  * This information would potentially be stored in a cookie to identify a user during a
304                  * session or series of sessions. It's uniqueness should not be depended on.
305                  * 
306                  * @return string random set of 32 alpha-numeric characters
307                  */
308                 function generateId() {
309                         var id = '';
310                         var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
311                         for ( var i = 0, r; i < 32; i++ ) {
312                                 r = Math.floor( Math.random() * seed.length );
313                                 id += seed.substring( r, r + 1 );
314                         }
315                         return id;
316                 }
318                 /**
319                  * Gets the current user's name.
320                  * 
321                  * @return mixed user name string or null if users is anonymous
322                  */
323                 this.name = function() {
324                         return mediaWiki.config.get( 'wgUserName' );
325                 };
327                 /**
328                  * Checks if the current user is anonymous.
329                  * 
330                  * @return boolean
331                  */
332                 this.anonymous = function() {
333                         return that.name() ? false : true;
334                 };
336                 /**
337                  * Gets a random session ID automatically generated and kept in a cookie.
338                  * 
339                  * This ID is ephemeral for everyone, staying in their browser only until they close
340                  * their browser.
341                  * 
342                  * Do not use this method before the first call to mediaWiki.loader.go(), it depends on
343                  * jquery.cookie, which is added to the first pay-load just after mediaWiki is defined, but
344                  * won't be loaded until the first call to go().
345                  * 
346                  * @return string user name or random session ID
347                  */
348                 this.sessionId = function () {
349                         var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
350                         if ( typeof sessionId == 'undefined' || sessionId == null ) {
351                                 sessionId = generateId();
352                                 $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } );
353                         }
354                         return sessionId;
355                 };
357                 /**
358                  * Gets the current user's name or a random ID automatically generated and kept in a cookie.
359                  * 
360                  * This ID is persistent for anonymous users, staying in their browser up to 1 year. The
361                  * expiration time is reset each time the ID is queried, so in most cases this ID will
362                  * persist until the browser's cookies are cleared or the user doesn't visit for 1 year.
363                  * 
364                  * Do not use this method before the first call to mediaWiki.loader.go(), it depends on
365                  * jquery.cookie, which is added to the first pay-load just after mediaWiki is defined, but
366                  * won't be loaded until the first call to go().
367                  * 
368                  * @return string user name or random session ID
369                  */
370                 this.id = function() {
371                         var name = that.name();
372                         if ( name ) {
373                                 return name;
374                         }
375                         var id = $.cookie( 'mediaWiki.user.id' );
376                         if ( typeof id == 'undefined' || id == null ) {
377                                 id = generateId();
378                         }
379                         // Set cookie if not set, or renew it if already set
380                         $.cookie( 'mediaWiki.user.id', id, { 'expires': 365, 'path': '/' } );
381                         return id;
382                 };
383         }
385         /* Public Members */
387         /*
388          * Dummy function which in debug mode can be replaced with a function that
389          * does something clever
390          */
391         this.log = function() { };
393         /*
394          * Make the Map-class publicly available
395          */
396         this.Map = Map;
398         /*
399          * List of configuration values
400          *
401          * In legacy mode the values this object wraps will be in the global space
402          */
403         this.config = new this.Map( LEGACY_GLOBALS );
405         /*
406          * Information about the current user
407          */
408         this.user = new User();
410         /*
411          * Localization system
412          */
413         this.messages = new this.Map();
415         /* Public Methods */
417         /**
418          * Gets a message object, similar to wfMessage()
419          *
420          * @param key string Key of message to get
421          * @param parameters mixed First argument in a list of variadic arguments, each a parameter for $
422          * replacement
423          */
424         this.message = function( key, parameters ) {
425                 // Support variadic arguments
426                 if ( typeof parameters !== 'undefined' ) {
427                         parameters = $.makeArray( arguments );
428                         parameters.shift();
429                 } else {
430                         parameters = [];
431                 }
432                 return new Message( mediaWiki.messages, key, parameters );
433         };
435         /**
436          * Gets a message string, similar to wfMsg()
437          *
438          * @param key string Key of message to get
439          * @param parameters mixed First argument in a list of variadic arguments, each a parameter for $
440          * replacement
441          */
442         this.msg = function( key, parameters ) {
443                 return mediaWiki.message.apply( mediaWiki.message, arguments ).toString();
444         };
446         /**
447          * Client-side module loader which integrates with the MediaWiki ResourceLoader
448          */
449         this.loader = new ( function() {
451                 /* Private Members */
453                 /**
454                  * Mapping of registered modules
455                  *
456                  * The jquery module is pre-registered, because it must have already
457                  * been provided for this object to have been built, and in debug mode
458                  * jquery would have been provided through a unique loader request,
459                  * making it impossible to hold back registration of jquery until after
460                  * mediawiki.
461                  *
462                  * Format:
463                  *      {
464                  *              'moduleName': {
465                  *                      'dependencies': ['required module', 'required module', ...], (or) function() {}
466                  *                      'state': 'registered', 'loading', 'loaded', 'ready', or 'error'
467                  *                      'script': function() {},
468                  *                      'style': 'css code string',
469                  *                      'messages': { 'key': 'value' },
470                  *                      'version': ############## (unix timestamp)
471                  *              }
472                  *      }
473                  */
474                 var registry = {};
475                 // List of modules which will be loaded as when ready
476                 var batch = [];
477                 // List of modules to be loaded
478                 var queue = [];
479                 // List of callback functions waiting for modules to be ready to be called
480                 var jobs = [];
481                 // Flag indicating that requests should be suspended
482                 var suspended = true;
483                 // Flag inidicating that document ready has occured
484                 var ready = false;
485                 // Marker element for adding dynamic styles
486                 var $marker = $( 'head meta[name=ResourceLoaderDynamicStyles]' );
488                 /* Private Methods */
490                 function compare( a, b ) {
491                         if ( a.length != b.length ) {
492                                 return false;
493                         }
494                         for ( var i = 0; i < b.length; i++ ) {
495                                 if ( $.isArray( a[i] ) ) {
496                                         if ( !compare( a[i], b[i] ) ) {
497                                                 return false;
498                                         }
499                                 }
500                                 if ( a[i] !== b[i] ) {
501                                         return false;
502                                 }
503                         }
504                         return true;
505                 }
507                 /**
508                  * Generates an ISO8601 "basic" string from a UNIX timestamp
509                  */
510                 function formatVersionNumber( timestamp ) {
511                         function pad( a, b, c ) {
512                                 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
513                         }
514                         var d = new Date();
515                         d.setTime( timestamp * 1000 );
516                         return [
517                                 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
518                                 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
519                         ].join( '' );
520                 }
522                 /**
523                  * Recursively resolves dependencies and detects circular references
524                  */
525                 function recurse( module, resolved, unresolved ) {
526                         if ( typeof registry[module] === 'undefined' ) {
527                                 throw new Error( 'Unknown dependency: ' + module );
528                         }
529                         // Resolves dynamic loader function and replaces it with its own results
530                         if ( typeof registry[module].dependencies === 'function' ) {
531                                 registry[module].dependencies = registry[module].dependencies();
532                                 // Ensures the module's dependencies are always in an array
533                                 if ( typeof registry[module].dependencies !== 'object' ) {
534                                         registry[module].dependencies = [registry[module].dependencies];
535                                 }
536                         }
537                         // Tracks down dependencies
538                         for ( var n = 0; n < registry[module].dependencies.length; n++ ) {
539                                 if ( $.inArray( registry[module].dependencies[n], resolved ) === -1 ) {
540                                         if ( $.inArray( registry[module].dependencies[n], unresolved ) !== -1 ) {
541                                                 throw new Error(
542                                                         'Circular reference detected: ' + module +
543                                                         ' -> ' + registry[module].dependencies[n]
544                                                 );
545                                         }
546                                         recurse( registry[module].dependencies[n], resolved, unresolved );
547                                 }
548                         }
549                         resolved[resolved.length] = module;
550                         unresolved.splice( $.inArray( module, unresolved ), 1 );
551                 }
553                 /**
554                  * Gets a list of module names that a module depends on in their proper dependency order
555                  *
556                  * @param module string module name or array of string module names
557                  * @return list of dependencies
558                  * @throws Error if circular reference is detected
559                  */
560                 function resolve( module, resolved, unresolved ) {
561                         // Allow calling with an array of module names
562                         if ( typeof module === 'object' ) {
563                                 var modules = [];
564                                 for ( var m = 0; m < module.length; m++ ) {
565                                         var dependencies = resolve( module[m] );
566                                         for ( var n = 0; n < dependencies.length; n++ ) {
567                                                 modules[modules.length] = dependencies[n];
568                                         }
569                                 }
570                                 return modules;
571                         } else if ( typeof module === 'string' ) {
572                                 // Undefined modules have no dependencies
573                                 if ( !( module in registry ) ) {
574                                         return [];
575                                 }
576                                 var resolved = [];
577                                 recurse( module, resolved, [] );
578                                 return resolved;
579                         }
580                         throw new Error( 'Invalid module argument: ' + module );
581                 }
583                 /**
584                  * Narrows a list of module names down to those matching a specific
585                  * state. Possible states are 'undefined', 'registered', 'loading',
586                  * 'loaded', or 'ready'
587                  *
588                  * @param states string or array of strings of module states to filter by
589                  * @param modules array list of module names to filter (optional, all modules
590                  *   will be used by default)
591                  * @return array list of filtered module names
592                  */
593                 function filter( states, modules ) {
594                         // Allow states to be given as a string
595                         if ( typeof states === 'string' ) {
596                                 states = [states];
597                         }
598                         // If called without a list of modules, build and use a list of all modules
599                         var list = [];
600                         if ( typeof modules === 'undefined' ) {
601                                 modules = [];
602                                 for ( module in registry ) {
603                                         modules[modules.length] = module;
604                                 }
605                         }
606                         // Build a list of modules which are in one of the specified states
607                         for ( var s = 0; s < states.length; s++ ) {
608                                 for ( var m = 0; m < modules.length; m++ ) {
609                                         if ( typeof registry[modules[m]] === 'undefined' ) {
610                                                 // Module does not exist
611                                                 if ( states[s] == 'undefined' ) {
612                                                         // OK, undefined
613                                                         list[list.length] = modules[m];
614                                                 }
615                                         } else {
616                                                 // Module exists, check state
617                                                 if ( registry[modules[m]].state === states[s] ) {
618                                                         // OK, correct state
619                                                         list[list.length] = modules[m];
620                                                 }
621                                         }
622                                 }
623                         }
624                         return list;
625                 }
627                 /**
628                  * Executes a loaded module, making it ready to use
629                  *
630                  * @param module string module name to execute
631                  */
632                 function execute( module ) {
633                         if ( typeof registry[module] === 'undefined' ) {
634                                 throw new Error( 'Module has not been registered yet: ' + module );
635                         } else if ( registry[module].state === 'registered' ) {
636                                 throw new Error( 'Module has not been requested from the server yet: ' + module );
637                         } else if ( registry[module].state === 'loading' ) {
638                                 throw new Error( 'Module has not completed loading yet: ' + module );
639                         } else if ( registry[module].state === 'ready' ) {
640                                 throw new Error( 'Module has already been loaded: ' + module );
641                         }
642                         // Add style sheet to document
643                         if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
644                                 $marker.before( mediaWiki.html.element( 'style',
645                                                 { type: 'text/css' },
646                                                 new mediaWiki.html.Cdata( registry[module].style )
647                                         ) );
648                         } else if ( typeof registry[module].style === 'object'
649                                 && !( registry[module].style instanceof Array ) )
650                         {
651                                 for ( var media in registry[module].style ) {
652                                         $marker.before( mediaWiki.html.element( 'style',
653                                                 { type: 'text/css', media: media },
654                                                 new mediaWiki.html.Cdata( registry[module].style[media] )
655                                         ) );
656                                 }
657                         }
658                         // Add localizations to message system
659                         if ( typeof registry[module].messages === 'object' ) {
660                                 mediaWiki.messages.set( registry[module].messages );
661                         }
662                         // Execute script
663                         try {
664                                 registry[module].script( jQuery, mediaWiki );
665                                 registry[module].state = 'ready';
666                                 // Run jobs who's dependencies have just been met
667                                 for ( var j = 0; j < jobs.length; j++ ) {
668                                         if ( compare(
669                                                 filter( 'ready', jobs[j].dependencies ),
670                                                 jobs[j].dependencies ) )
671                                         {
672                                                 if ( typeof jobs[j].ready === 'function' ) {
673                                                         jobs[j].ready();
674                                                 }
675                                                 jobs.splice( j, 1 );
676                                                 j--;
677                                         }
678                                 }
679                                 // Execute modules who's dependencies have just been met
680                                 for ( r in registry ) {
681                                         if ( registry[r].state == 'loaded' ) {
682                                                 if ( compare(
683                                                         filter( ['ready'], registry[r].dependencies ),
684                                                         registry[r].dependencies ) )
685                                                 {
686                                                         execute( r );
687                                                 }
688                                         }
689                                 }
690                         } catch ( e ) {
691                                 mediaWiki.log( 'Exception thrown by ' + module + ': ' + e.message );
692                                 mediaWiki.log( e );
693                                 registry[module].state = 'error';
694                                 // Run error callbacks of jobs affected by this condition
695                                 for ( var j = 0; j < jobs.length; j++ ) {
696                                         if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) {
697                                                 if ( typeof jobs[j].error === 'function' ) {
698                                                         jobs[j].error();
699                                                 }
700                                                 jobs.splice( j, 1 );
701                                                 j--;
702                                         }
703                                 }
704                         }
705                 }
707                 /**
708                  * Adds a dependencies to the queue with optional callbacks to be run
709                  * when the dependencies are ready or fail
710                  *
711                  * @param dependencies string module name or array of string module names
712                  * @param ready function callback to execute when all dependencies are ready
713                  * @param error function callback to execute when any dependency fails
714                  */
715                 function request( dependencies, ready, error ) {
716                         // Allow calling by single module name
717                         if ( typeof dependencies === 'string' ) {
718                                 dependencies = [dependencies];
719                                 if ( dependencies[0] in registry ) {
720                                         for ( var n = 0; n < registry[dependencies[0]].dependencies.length; n++ ) {
721                                                 dependencies[dependencies.length] =
722                                                         registry[dependencies[0]].dependencies[n];
723                                         }
724                                 }
725                         }
726                         // Add ready and error callbacks if they were given
727                         if ( arguments.length > 1 ) {
728                                 jobs[jobs.length] = {
729                                         'dependencies': filter(
730                                                 ['undefined', 'registered', 'loading', 'loaded'],
731                                                 dependencies ),
732                                         'ready': ready,
733                                         'error': error
734                                 };
735                         }
736                         // Queue up any dependencies that are undefined or registered
737                         dependencies = filter( ['undefined', 'registered'], dependencies );
738                         for ( var n = 0; n < dependencies.length; n++ ) {
739                                 if ( $.inArray( dependencies[n], queue ) === -1 ) {
740                                         queue[queue.length] = dependencies[n];
741                                 }
742                         }
743                         // Work the queue
744                         mediaWiki.loader.work();
745                 }
747                 function sortQuery(o) {
748                         var sorted = {}, key, a = [];
749                         for ( key in o ) {
750                                 if ( o.hasOwnProperty( key ) ) {
751                                         a.push( key );
752                                 }
753                         }
754                         a.sort();
755                         for ( key = 0; key < a.length; key++ ) {
756                                 sorted[a[key]] = o[a[key]];
757                         }
758                         return sorted;
759                 }
761                 /* Public Methods */
763                 /**
764                  * Requests dependencies from server, loading and executing when things when ready.
765                  */
766                 this.work = function() {
767                         // Appends a list of modules to the batch
768                         for ( var q = 0; q < queue.length; q++ ) {
769                                 // Only request modules which are undefined or registered
770                                 if ( !( queue[q] in registry ) || registry[queue[q]].state == 'registered' ) {
771                                         // Prevent duplicate entries
772                                         if ( $.inArray( queue[q], batch ) === -1 ) {
773                                                 batch[batch.length] = queue[q];
774                                                 // Mark registered modules as loading
775                                                 if ( queue[q] in registry ) {
776                                                         registry[queue[q]].state = 'loading';
777                                                 }
778                                         }
779                                 }
780                         }
781                         // Clean up the queue
782                         queue = [];
783                         // After document ready, handle the batch
784                         if ( !suspended && batch.length ) {
785                                 // Always order modules alphabetically to help reduce cache
786                                 // misses for otherwise identical content
787                                 batch.sort();
788                                 // Build a list of request parameters
789                                 var base = {
790                                         'skin': mediaWiki.config.get( 'skin' ),
791                                         'lang': mediaWiki.config.get( 'wgUserLanguage' ),
792                                         'debug': mediaWiki.config.get( 'debug' )
793                                 };
794                                 // Extend request parameters with a list of modules in the batch
795                                 var requests = [];
796                                 // Split into groups
797                                 var groups = {};
798                                 for ( var b = 0; b < batch.length; b++ ) {
799                                         var group = registry[batch[b]].group;
800                                         if ( !( group in groups ) ) {
801                                                 groups[group] = [];
802                                         }
803                                         groups[group][groups[group].length] = batch[b];
804                                 }
805                                 for ( var group in groups ) {
806                                         // Calculate the highest timestamp
807                                         var version = 0;
808                                         for ( var g = 0; g < groups[group].length; g++ ) {
809                                                 if ( registry[groups[group][g]].version > version ) {
810                                                         version = registry[groups[group][g]].version;
811                                                 }
812                                         }
813                                         requests[requests.length] = $.extend(
814                                                 { 'modules': groups[group].join( '|' ), 'version': formatVersionNumber( version ) }, base
815                                         );
816                                 }
817                                 // Clear the batch - this MUST happen before we append the
818                                 // script element to the body or it's possible that the script
819                                 // will be locally cached, instantly load, and work the batch
820                                 // again, all before we've cleared it causing each request to
821                                 // include modules which are already loaded
822                                 batch = [];
823                                 // Asynchronously append a script tag to the end of the body
824                                 function request() {
825                                         var html = '';
826                                         for ( var r = 0; r < requests.length; r++ ) {
827                                                 requests[r] = sortQuery( requests[r] );
828                                                 // Build out the HTML
829                                                 var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] );
830                                                 html += mediaWiki.html.element( 'script',
831                                                         { type: 'text/javascript', src: src }, '' );
832                                         }
833                                         return html;
834                                 }
835                                 // Load asynchronously after doumument ready
836                                 if ( ready ) {
837                                         setTimeout( function() { $( 'body' ).append( request() ); }, 0 )
838                                 } else {
839                                         document.write( request() );
840                                 }
841                         }
842                 };
844                 /**
845                  * Registers a module, letting the system know about it and its
846                  * dependencies. loader.js files contain calls to this function.
847                  */
848                 this.register = function( module, version, dependencies, group ) {
849                         // Allow multiple registration
850                         if ( typeof module === 'object' ) {
851                                 for ( var m = 0; m < module.length; m++ ) {
852                                         if ( typeof module[m] === 'string' ) {
853                                                 mediaWiki.loader.register( module[m] );
854                                         } else if ( typeof module[m] === 'object' ) {
855                                                 mediaWiki.loader.register.apply( mediaWiki.loader, module[m] );
856                                         }
857                                 }
858                                 return;
859                         }
860                         // Validate input
861                         if ( typeof module !== 'string' ) {
862                                 throw new Error( 'module must be a string, not a ' + typeof module );
863                         }
864                         if ( typeof registry[module] !== 'undefined' ) {
865                                 throw new Error( 'module already implemeneted: ' + module );
866                         }
867                         // List the module as registered
868                         registry[module] = {
869                                 'state': 'registered',
870                                 'group': typeof group === 'string' ? group : null,
871                                 'dependencies': [],
872                                 'version': typeof version !== 'undefined' ? parseInt( version ) : 0
873                         };
874                         if ( typeof dependencies === 'string' ) {
875                                 // Allow dependencies to be given as a single module name
876                                 registry[module].dependencies = [dependencies];
877                         } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
878                                 // Allow dependencies to be given as an array of module names
879                                 // or a function which returns an array
880                                 registry[module].dependencies = dependencies;
881                         }
882                 };
884                 /**
885                  * Implements a module, giving the system a course of action to take
886                  * upon loading. Results of a request for one or more modules contain
887                  * calls to this function.
888                  */
889                 this.implement = function( module, script, style, localization ) {
890                         // Automatically register module
891                         if ( typeof registry[module] === 'undefined' ) {
892                                 mediaWiki.loader.register( module );
893                         }
894                         // Validate input
895                         if ( typeof script !== 'function' ) {
896                                 throw new Error( 'script must be a function, not a ' + typeof script );
897                         }
898                         if ( typeof style !== 'undefined'
899                                 && typeof style !== 'string'
900                                 && typeof style !== 'object' )
901                         {
902                                 throw new Error( 'style must be a string or object, not a ' + typeof style );
903                         }
904                         if ( typeof localization !== 'undefined'
905                                 && typeof localization !== 'object' )
906                         {
907                                 throw new Error( 'localization must be an object, not a ' + typeof localization );
908                         }
909                         if ( typeof registry[module] !== 'undefined'
910                                 && typeof registry[module].script !== 'undefined' )
911                         {
912                                 throw new Error( 'module already implemeneted: ' + module );
913                         }
914                         // Mark module as loaded
915                         registry[module].state = 'loaded';
916                         // Attach components
917                         registry[module].script = script;
918                         if ( typeof style === 'string'
919                                 || typeof style === 'object' && !( style instanceof Array ) )
920                         {
921                                 registry[module].style = style;
922                         }
923                         if ( typeof localization === 'object' ) {
924                                 registry[module].messages = localization;
925                         }
926                         // Execute or queue callback
927                         if ( compare(
928                                 filter( ['ready'], registry[module].dependencies ),
929                                 registry[module].dependencies ) )
930                         {
931                                 execute( module );
932                         } else {
933                                 request( module );
934                         }
935                 };
937                 /**
938                  * Executes a function as soon as one or more required modules are ready
939                  *
940                  * @param dependencies string or array of strings of modules names the callback
941                  *   dependencies to be ready before
942                  * executing
943                  * @param ready function callback to execute when all dependencies are ready (optional)
944                  * @param error function callback to execute when if dependencies have a errors (optional)
945                  */
946                 this.using = function( dependencies, ready, error ) {
947                         // Validate input
948                         if ( typeof dependencies !== 'object' && typeof dependencies !== 'string' ) {
949                                 throw new Error( 'dependencies must be a string or an array, not a ' +
950                                         typeof dependencies )
951                         }
952                         // Allow calling with a single dependency as a string
953                         if ( typeof dependencies === 'string' ) {
954                                 dependencies = [dependencies];
955                         }
956                         // Resolve entire dependency map
957                         dependencies = resolve( dependencies );
958                         // If all dependencies are met, execute ready immediately
959                         if ( compare( filter( ['ready'], dependencies ), dependencies ) ) {
960                                 if ( typeof ready === 'function' ) {
961                                         ready();
962                                 }
963                         }
964                         // If any dependencies have errors execute error immediately
965                         else if ( filter( ['error'], dependencies ).length ) {
966                                 if ( typeof error === 'function' ) {
967                                         error();
968                                 }
969                         }
970                         // Since some dependencies are not yet ready, queue up a request
971                         else {
972                                 request( dependencies, ready, error );
973                         }
974                 };
976                 /**
977                  * Loads an external script or one or more modules for future use
978                  *
979                  * @param modules mixed either the name of a module, array of modules,
980                  *   or a URL of an external script or style
981                  * @param type string mime-type to use if calling with a URL of an
982                  *   external script or style; acceptable values are "text/css" and
983                  *   "text/javascript"; if no type is provided, text/javascript is
984                  *   assumed
985                  */
986                 this.load = function( modules, type ) {
987                         // Validate input
988                         if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
989                                 throw new Error( 'dependencies must be a string or an array, not a ' +
990                                         typeof dependencies )
991                         }
992                         // Allow calling with an external script or single dependency as a string
993                         if ( typeof modules === 'string' ) {
994                                 // Support adding arbitrary external scripts
995                                 if ( modules.substr( 0, 7 ) == 'http://'
996                                         || modules.substr( 0, 8 ) == 'https://' )
997                                 {
998                                         if ( type === 'text/css' ) {
999                                                 $( 'head' )
1000                                                         .append( $( '<link rel="stylesheet" type="text/css" />' )
1001                                                         .attr( 'href', modules ) );
1002                                                 return true;
1003                                         } else if ( type === 'text/javascript' || typeof type === 'undefined' ) {
1004                                                 var script = mediaWiki.html.element( 'script',
1005                                                         { type: 'text/javascript', src: modules }, '' );
1006                                                 if ( ready ) {
1007                                                         $( 'body' ).append( script );
1008                                                 } else {
1009                                                         document.write( script );
1010                                                 }
1011                                                 return true;
1012                                         }
1013                                         // Unknown type
1014                                         return false;
1015                                 }
1016                                 // Called with single module
1017                                 modules = [modules];
1018                         }
1019                         // Resolve entire dependency map
1020                         modules = resolve( modules );
1021                         // If all modules are ready, nothing dependency be done
1022                         if ( compare( filter( ['ready'], modules ), modules ) ) {
1023                                 return true;
1024                         }
1025                         // If any modules have errors return false
1026                         else if ( filter( ['error'], modules ).length ) {
1027                                 return false;
1028                         }
1029                         // Since some modules are not yet ready, queue up a request
1030                         else {
1031                                 request( modules );
1032                                 return true;
1033                         }
1034                 };
1036                 /**
1037                  * Flushes the request queue and begin executing load requests on demand
1038                  */
1039                 this.go = function() {
1040                         suspended = false;
1041                         mediaWiki.loader.work();
1042                 };
1044                 /**
1045                  * Changes the state of a module
1046                  *
1047                  * @param module string module name or object of module name/state pairs
1048                  * @param state string state name
1049                  */
1050                 this.state = function( module, state ) {
1051                         if ( typeof module === 'object' ) {
1052                                 for ( var m in module ) {
1053                                         mediaWiki.loader.state( m, module[m] );
1054                                 }
1055                                 return;
1056                         }
1057                         if ( !( module in registry ) ) {
1058                                 mediaWiki.loader.register( module );
1059                         }
1060                         registry[module].state = state;
1061                 };
1063                 /**
1064                  * Gets the version of a module
1065                  *
1066                  * @param module string name of module to get version for
1067                  */
1068                 this.version = function( module ) {
1069                         if ( module in registry && 'version' in registry[module] ) {
1070                                 return formatVersionNumber( registry[module].version );
1071                         }
1072                         return null;
1073                 };
1075                 /* Cache document ready status */
1077                 $(document).ready( function() { ready = true; } );
1078         } )();
1080         /** HTML construction helper functions */
1081         this.html = new ( function () {
1082                 function escapeCallback( s ) {
1083                         switch ( s ) {
1084                                 case "'":
1085                                         return '&#039;';
1086                                 case '"':
1087                                         return '&quot;';
1088                                 case '<':
1089                                         return '&lt;';
1090                                 case '>':
1091                                         return '&gt;';
1092                                 case '&':
1093                                         return '&amp;';
1094                         }
1095                 }
1097                 /**
1098                  * Escape a string for HTML. Converts special characters to HTML entities.
1099                  * @param s The string to escape
1100                  */
1101                 this.escape = function( s ) {
1102                         return s.replace( /['"<>&]/g, escapeCallback );
1103                 };
1105                 /**
1106                  * Wrapper object for raw HTML passed to mediaWiki.html.element().
1107                  */
1108                 this.Raw = function( value ) {
1109                         this.value = value;
1110                 };
1112                 /**
1113                  * Wrapper object for CDATA element contents passed to mediaWiki.html.element()
1114                  */
1115                 this.Cdata = function( value ) {
1116                         this.value = value;
1117                 };
1119                 /**
1120                  * Create an HTML element string, with safe escaping.
1121                  *
1122                  * @param name The tag name.
1123                  * @param attrs An object with members mapping element names to values
1124                  * @param contents The contents of the element. May be either:
1125                  *    - string: The string is escaped.
1126                  *    - null or undefined: The short closing form is used, e.g. <br/>.
1127                  *    - this.Raw: The value attribute is included without escaping.
1128                  *    - this.Cdata: The value attribute is included, and an exception is
1129                  *      thrown if it contains an illegal ETAGO delimiter.
1130                  *      See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1131                  *
1132                  * Example:
1133                  *    var h = mediaWiki.html;
1134                  *    return h.element( 'div', {},
1135                  *        new h.Raw( h.element( 'img', {src: '<'} ) ) );
1136                  * Returns <div><img src="&lt;"/></div>
1137                  */
1138                 this.element = function( name, attrs, contents ) {
1139                         var s = '<' + name;
1140                         for ( var attrName in attrs ) {
1141                                 s += ' ' + attrName + '="' + this.escape( attrs[attrName] ) + '"';
1142                         }
1143                         if ( typeof contents == 'undefined' || contents === null ) {
1144                                 // Self close tag
1145                                 s += '/>';
1146                                 return s;
1147                         }
1148                         // Regular open tag
1149                         s += '>';
1150                         if ( typeof contents === 'string') {
1151                                 // Escaped
1152                                 s += this.escape( contents );
1153                         } else if ( contents instanceof this.Raw ) {
1154                                 // Raw HTML inclusion
1155                                 s += contents.value;
1156                         } else if ( contents instanceof this.Cdata ) {
1157                                 // CDATA
1158                                 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1159                                         throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1160                                 }
1161                                 s += contents.value;
1162                         } else {
1163                                 throw new Error( 'mw.html.element: Invalid type of contents' );
1164                         }
1165                         s += '</' + name + '>';
1166                         return s;
1167                 };
1168         } )();
1171         /* Extension points */
1173         this.legacy = {};
1175 } )( jQuery );
1177 /* Auto-register from pre-loaded startup scripts */
1179 if ( typeof startUp === 'function' ) {
1180         startUp();
1181         delete startUp;
1184 // Add jQuery Cookie to initial payload (used in mediaWiki.user)
1185 mediaWiki.loader.load( 'jquery.cookie' );
1187 // Alias $j to jQuery for backwards compatibility
1188 window.$j = jQuery;
1189 window.mw = mediaWiki;