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