+
+ /* Public Methods */
+ return {
+ /**
+ * Requests dependencies from server, loading and executing when things when ready.
+ */
+ work: function () {
+ var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
+ source, group, g, i, modules, maxVersion, sourceLoadScript,
+ currReqBase, currReqBaseLength, moduleMap, l,
+ lastDotIndex, prefix, suffix, bytesAdded;
+
+ // Build a list of request parameters common to all requests.
+ reqBase = {
+ skin: mw.config.get( 'skin' ),
+ lang: mw.config.get( 'wgUserLanguage' ),
+ debug: mw.config.get( 'debug' )
+ };
+ // Split module batch by source and by group.
+ splits = {};
+ maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
+
+ // Appends a list of modules from the queue to the batch
+ for ( q = 0; q < queue.length; q += 1 ) {
+ // Only request modules which are registered
+ if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
+ // Prevent duplicate entries
+ if ( $.inArray( queue[q], batch ) === -1 ) {
+ batch[batch.length] = queue[q];
+ // Mark registered modules as loading
+ registry[queue[q]].state = 'loading';
+ }
+ }
+ }
+ // Early exit if there's nothing to load...
+ if ( !batch.length ) {
+ return;
+ }
+
+ // The queue has been processed into the batch, clear up the queue.
+ queue = [];
+
+ // Always order modules alphabetically to help reduce cache
+ // misses for otherwise identical content.
+ batch.sort();
+
+ // Split batch by source and by group.
+ for ( b = 0; b < batch.length; b += 1 ) {
+ bSource = registry[batch[b]].source;
+ bGroup = registry[batch[b]].group;
+ if ( splits[bSource] === undefined ) {
+ splits[bSource] = {};
+ }
+ if ( splits[bSource][bGroup] === undefined ) {
+ splits[bSource][bGroup] = [];
+ }
+ bSourceGroup = splits[bSource][bGroup];
+ bSourceGroup[bSourceGroup.length] = batch[b];
+ }
+
+ // Clear the batch - this MUST happen before we append any
+ // script elements to the body or it's possible that a script
+ // will be locally cached, instantly load, and work the batch
+ // again, all before we've cleared it causing each request to
+ // include modules which are already loaded.
+ batch = [];
+
+ for ( source in splits ) {
+
+ sourceLoadScript = sources[source].loadScript;
+
+ for ( group in splits[source] ) {
+
+ // Cache access to currently selected list of
+ // modules for this group from this source.
+ modules = splits[source][group];
+
+ // Calculate the highest timestamp
+ maxVersion = 0;
+ for ( g = 0; g < modules.length; g += 1 ) {
+ if ( registry[modules[g]].version > maxVersion ) {
+ maxVersion = registry[modules[g]].version;
+ }
+ }
+
+ currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase );
+ currReqBaseLength = $.param( currReqBase ).length;
+ moduleMap = {};
+ // We may need to split up the request to honor the query string length limit,
+ // so build it piece by piece.
+ l = currReqBaseLength + 9; // '&modules='.length == 9
+
+ moduleMap = {}; // { prefix: [ suffixes ] }
+
+ for ( i = 0; i < modules.length; i += 1 ) {
+ // Determine how many bytes this module would add to the query string
+ lastDotIndex = modules[i].lastIndexOf( '.' );
+ // Note that these substr() calls work even if lastDotIndex == -1
+ prefix = modules[i].substr( 0, lastDotIndex );
+ suffix = modules[i].substr( lastDotIndex + 1 );
+ bytesAdded = moduleMap[prefix] !== undefined
+ ? suffix.length + 3 // '%2C'.length == 3
+ : modules[i].length + 3; // '%7C'.length == 3
+
+ // If the request would become too long, create a new one,
+ // but don't create empty requests
+ if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
+ // This request would become too long, create a new one
+ // and fire off the old one
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ moduleMap = {};
+ l = currReqBaseLength + 9;
+ }
+ if ( moduleMap[prefix] === undefined ) {
+ moduleMap[prefix] = [];
+ }
+ moduleMap[prefix].push( suffix );
+ l += bytesAdded;
+ }
+ // If there's anything left in moduleMap, request that too
+ if ( !$.isEmptyObject( moduleMap ) ) {
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ }
+ }
+ }
+ },
+
+ /**
+ * Register a source.
+ *
+ * @param id {String}: Short lowercase a-Z string representing a source, only used internally.
+ * @param props {Object}: Object containing only the loadScript property which is a url to
+ * the load.php location of the source.
+ * @return {Boolean}
+ */
+ addSource: function ( id, props ) {
+ var source;
+ // Allow multiple additions
+ if ( typeof id === 'object' ) {
+ for ( source in id ) {
+ mw.loader.addSource( source, id[source] );
+ }
+ return true;
+ }
+
+ if ( sources[id] !== undefined ) {
+ throw new Error( 'source already registered: ' + id );
+ }
+
+ sources[id] = props;
+
+ return true;
+ },
+
+ /**
+ * Registers a module, letting the system know about it and its
+ * properties. Startup modules contain calls to this function.
+ *
+ * @param module {String}: Module name
+ * @param version {Number}: Module version number as a timestamp (falls backs to 0)
+ * @param dependencies {String|Array|Function}: One string or array of strings of module
+ * names on which this module depends, or a function that returns that array.
+ * @param group {String}: Group which the module is in (optional, defaults to null)
+ * @param source {String}: Name of the source. Defaults to local.
+ */
+ register: function ( module, version, dependencies, group, source ) {
+ var m;
+ // Allow multiple registration
+ if ( typeof module === 'object' ) {
+ for ( m = 0; m < module.length; m += 1 ) {
+ // module is an array of module names
+ if ( typeof module[m] === 'string' ) {
+ mw.loader.register( module[m] );
+ // module is an array of arrays
+ } else if ( typeof module[m] === 'object' ) {
+ mw.loader.register.apply( mw.loader, module[m] );
+ }
+ }
+ return;
+ }
+ // Validate input
+ if ( typeof module !== 'string' ) {
+ throw new Error( 'module must be a string, not a ' + typeof module );
+ }
+ if ( registry[module] !== undefined ) {
+ throw new Error( 'module already registered: ' + module );
+ }
+ // List the module as registered
+ registry[module] = {
+ 'version': version !== undefined ? parseInt( version, 10 ) : 0,
+ 'dependencies': [],
+ 'group': typeof group === 'string' ? group : null,
+ 'source': typeof source === 'string' ? source: 'local',
+ 'state': 'registered'
+ };
+ if ( typeof dependencies === 'string' ) {
+ // Allow dependencies to be given as a single module name
+ registry[module].dependencies = [dependencies];
+ } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
+ // Allow dependencies to be given as an array of module names
+ // or a function which returns an array
+ registry[module].dependencies = dependencies;
+ }
+ },
+
+ /**
+ * Implements a module, giving the system a course of action to take
+ * upon loading. Results of a request for one or more modules contain
+ * calls to this function.
+ *
+ * All arguments are required.
+ *
+ * @param module String: Name of module
+ * @param script Mixed: Function of module code or String of URL to be used as the src
+ * attribute when adding a script element to the body
+ * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs
+ * keyed by media-type
+ * @param msgs Object: List of key/value pairs to be passed through mw.messages.set
+ */
+ implement: function ( module, script, style, msgs ) {
+ // Validate input
+ if ( typeof module !== 'string' ) {
+ throw new Error( 'module must be a string, not a ' + typeof module );
+ }
+ if ( !$.isFunction( script ) && !$.isArray( script ) ) {
+ throw new Error( 'script must be a function or an array, not a ' + typeof script );
+ }
+ if ( !$.isPlainObject( style ) ) {
+ throw new Error( 'style must be an object, not a ' + typeof style );
+ }
+ if ( !$.isPlainObject( msgs ) ) {
+ throw new Error( 'msgs must be an object, not a ' + typeof msgs );
+ }
+ // Automatically register module
+ if ( registry[module] === undefined ) {
+ mw.loader.register( module );
+ }
+ // Check for duplicate implementation
+ if ( registry[module] !== undefined && registry[module].script !== undefined ) {
+ throw new Error( 'module already implemented: ' + module );
+ }
+ // Mark module as loaded
+ registry[module].state = 'loaded';
+ // Attach components
+ registry[module].script = script;
+ registry[module].style = style;
+ registry[module].messages = msgs;
+ // Execute or queue callback
+ if ( compare(
+ filter( ['ready'], registry[module].dependencies ),
+ registry[module].dependencies ) )
+ {
+ execute( module );
+ }
+ },
+
+ /**
+ * Executes a function as soon as one or more required modules are ready
+ *
+ * @param dependencies {String|Array} Module name or array of modules names the callback
+ * dependends on to be ready before executing
+ * @param ready {Function} callback to execute when all dependencies are ready (optional)
+ * @param error {Function} callback to execute when if dependencies have a errors (optional)
+ */
+ using: function ( dependencies, ready, error ) {
+ var tod = typeof dependencies;
+ // Validate input
+ if ( tod !== 'object' && tod !== 'string' ) {
+ throw new Error( 'dependencies must be a string or an array, not a ' + tod );
+ }
+ // Allow calling with a single dependency as a string
+ if ( tod === 'string' ) {
+ dependencies = [dependencies];
+ }
+ // Resolve entire dependency map
+ dependencies = resolve( dependencies );
+ // If all dependencies are met, execute ready immediately
+ if ( compare( filter( ['ready'], dependencies ), dependencies ) ) {
+ if ( $.isFunction( ready ) ) {
+ ready();
+ }
+ }
+ // If any dependencies have errors execute error immediately
+ else if ( filter( ['error'], dependencies ).length ) {
+ if ( $.isFunction( error ) ) {
+ error( new Error( 'one or more dependencies have state "error"' ),
+ dependencies );
+ }
+ }
+ // Since some dependencies are not yet ready, queue up a request
+ else {
+ request( dependencies, ready, error );
+ }
+ },
+
+ /**
+ * Loads an external script or one or more modules for future use
+ *
+ * @param modules {mixed} Either the name of a module, array of modules,
+ * or a URL of an external script or style
+ * @param type {String} mime-type to use if calling with a URL of an
+ * external script or style; acceptable values are "text/css" and
+ * "text/javascript"; if no type is provided, text/javascript is assumed.
+ */
+ load: function ( modules, type ) {
+ var filtered, m;
+
+ // Validate input
+ if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
+ throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
+ }
+ // Allow calling with an external url or single dependency as a string
+ if ( typeof modules === 'string' ) {
+ // Support adding arbitrary external scripts
+ if ( /^(https?:)?\/\//.test( modules ) ) {
+ if ( type === 'text/css' ) {
+ $( 'head' ).append( $( '<link>', {
+ rel: 'stylesheet',
+ type: 'text/css',
+ href: modules
+ } ) );
+ return;
+ } else if ( type === 'text/javascript' || type === undefined ) {
+ addScript( modules );
+ return;
+ }
+ // Unknown type
+ throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
+ }
+ // Called with single module
+ modules = [modules];
+ }