Merge "resourceloader: Implement mwLoadEnd marker"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 8 Sep 2015 13:26:38 +0000 (13:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 8 Sep 2015 13:26:38 +0000 (13:26 +0000)
1  2 
resources/src/mediawiki/mediawiki.js

@@@ -69,7 -69,7 +69,7 @@@
  
                                if ( $.isPlainObject( selection ) ) {
                                        for ( s in selection ) {
 -                                              setGlobalMapValue( this, s, selection[s] );
 +                                              setGlobalMapValue( this, s, selection[ s ] );
                                        }
                                        return true;
                                }
@@@ -96,7 -96,7 +96,7 @@@
         * @param {Mixed} value
         */
        function setGlobalMapValue( map, key, value ) {
 -              map.values[key] = value;
 +              map.values[ key ] = value;
                mw.log.deprecate(
                                window,
                                key,
                                selection = slice.call( selection );
                                results = {};
                                for ( i = 0; i < selection.length; i++ ) {
 -                                      results[selection[i]] = this.get( selection[i], fallback );
 +                                      results[ selection[ i ] ] = this.get( selection[ i ], fallback );
                                }
                                return results;
                        }
                                if ( !hasOwn.call( this.values, selection ) ) {
                                        return fallback;
                                }
 -                              return this.values[selection];
 +                              return this.values[ selection ];
                        }
  
                        if ( selection === undefined ) {
  
                        if ( $.isPlainObject( selection ) ) {
                                for ( s in selection ) {
 -                                      this.values[s] = selection[s];
 +                                      this.values[ s ] = selection[ s ];
                                }
                                return true;
                        }
                        if ( typeof selection === 'string' && arguments.length > 1 ) {
 -                              this.values[selection] = value;
 +                              this.values[ selection ] = value;
                                return true;
                        }
                        return false;
  
                        if ( $.isArray( selection ) ) {
                                for ( s = 0; s < selection.length; s++ ) {
 -                                      if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
 +                                      if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
                                                return false;
                                        }
                                }
                params: function ( parameters ) {
                        var i;
                        for ( i = 0; i < parameters.length; i += 1 ) {
 -                              this.parameters.push( parameters[i] );
 +                              this.parameters.push( parameters[ i ] );
                        }
                        return this;
                },
                        var parameters = slice.call( arguments, 1 );
                        return formatString.replace( /\$(\d+)/g, function ( str, match ) {
                                var index = parseInt( match, 10 ) - 1;
 -                              return parameters[index] !== undefined ? parameters[index] : '$' + match;
 +                              return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
                        } );
                },
  
                 */
                trackUnsubscribe: function ( callback ) {
                        trackHandlers = $.grep( trackHandlers, function ( fns ) {
 -                              if ( fns[1] === callback ) {
 -                                      trackCallbacks.remove( fns[0] );
 +                              if ( fns[ 1 ] === callback ) {
 +                                      trackCallbacks.remove( fns[ 0 ] );
                                        // Ensure the tuple is removed to avoid holding on to closures
                                        return false;
                                }
  
                /**
                 * Dummy placeholder for {@link mw.log}
 +               *
                 * @method
                 */
                log: ( function () {
                         * @param {string} [msg] Optional text to include in the deprecation message
                         */
                        log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
 -                              obj[key] = val;
 +                              obj[ key ] = val;
                        } : function ( obj, key, val, msg ) {
                                msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
                                // Support: IE8
                                        } );
                                } catch ( err ) {
                                        // Fallback to creating a copy of the value to the object.
 -                                      obj[key] = val;
 +                                      obj[ key ] = val;
                                }
                        };
  
                         *
                         *     {
                         *         'moduleName': {
 -                       *             // From mw.loader.register() in startup module
 +                       *             // From mw.loader.register()
                         *             'version': '########' (hash)
                         *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
                         *             'group': 'somegroup', (or) null
                         *         }
                         *     }
                         *
 +                       * State machine:
 +                       *
 +                       * - `registered`:
 +                       *    The module is known to the system but not yet requested.
 +                       *    Meta data is registered via mw.loader#register. Calls to that method are
 +                       *    generated server-side by the startup module.
 +                       * - `loading`:
 +                       *    The module is requested through mw.loader (either directly or as dependency of
 +                       *    another module). The client will be fetching module contents from the server.
 +                       *    The contents are then stashed in the registry via mw.loader#implement.
 +                       * - `loaded`:
 +                       *    The module has been requested from the server and stashed via mw.loader#implement.
 +                       *    If the module has no more dependencies in-fight, the module will be executed
 +                       *    right away. Otherwise execution is deferred, controlled via #handlePending.
 +                       * - `executing`:
 +                       *    The module is being executed.
 +                       * - `ready`:
 +                       *    The module has been successfully executed.
 +                       * - `error`:
 +                       *    The module (or one of its dependencies) produced an error during execution.
 +                       * - `missing`:
 +                       *    The module was registered client-side and requested, but the server denied knowledge
 +                       *    of the module's existence.
 +                       *
                         * @property
                         * @private
                         */
                                if ( nextnode ) {
                                        $( nextnode ).before( s );
                                } else {
 -                                      document.getElementsByTagName( 'head' )[0].appendChild( s );
 +                                      document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
                                }
                                if ( s.styleSheet ) {
                                        // Support: IE6-10
                         */
                        function getCombinedVersion( modules ) {
                                var hashes = $.map( modules, function ( module ) {
 -                                      return registry[module].version;
 +                                      return registry[ module ].version;
                                } );
                                // Trim for consistency with server-side ResourceLoader::makeHash. It also helps
                                // save precious space in the limited query string. Otherwise modules are more
                        function allReady( modules ) {
                                var i;
                                for ( i = 0; i < modules.length; i++ ) {
 -                                      if ( mw.loader.getState( modules[i] ) !== 'ready' ) {
 +                                      if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
                                                return false;
                                        }
                                }
                        function anyFailed( modules ) {
                                var i, state;
                                for ( i = 0; i < modules.length; i++ ) {
 -                                      state = mw.loader.getState( modules[i] );
 +                                      state = mw.loader.getState( modules[ i ] );
                                        if ( state === 'error' || state === 'missing' ) {
                                                return true;
                                        }
                        function handlePending( module ) {
                                var j, job, hasErrors, m, stateChange;
  
 -                              if ( registry[module].state === 'error' || registry[module].state === 'missing' ) {
 +                              if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
                                        // If the current module failed, mark all dependent modules also as failed.
                                        // Iterate until steady-state to propagate the error state upwards in the
                                        // dependency tree.
                                        do {
                                                stateChange = false;
                                                for ( m in registry ) {
 -                                                      if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) {
 -                                                              if ( anyFailed( registry[m].dependencies ) ) {
 -                                                                      registry[m].state = 'error';
 +                                                      if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
 +                                                              if ( anyFailed( registry[ m ].dependencies ) ) {
 +                                                                      registry[ m ].state = 'error';
                                                                        stateChange = true;
                                                                }
                                                        }
  
                                // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
                                for ( j = 0; j < jobs.length; j += 1 ) {
 -                                      hasErrors = anyFailed( jobs[j].dependencies );
 -                                      if ( hasErrors || allReady( jobs[j].dependencies ) ) {
 +                                      hasErrors = anyFailed( jobs[ j ].dependencies );
 +                                      if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
                                                // All dependencies satisfied, or some have errors
 -                                              job = jobs[j];
 +                                              job = jobs[ j ];
                                                jobs.splice( j, 1 );
                                                j -= 1;
                                                try {
                                                        if ( hasErrors ) {
                                                                if ( $.isFunction( job.error ) ) {
 -                                                                      job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
 +                                                                      job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
                                                                }
                                                        } else {
                                                                if ( $.isFunction( job.ready ) ) {
                                        }
                                }
  
 -                              if ( registry[module].state === 'ready' ) {
 +                              if ( registry[ module ].state === 'ready' ) {
                                        // The current module became 'ready'. Set it in the module store, and recursively execute all
                                        // dependent modules that are loaded and now have all dependencies satisfied.
 -                                      mw.loader.store.set( module, registry[module] );
 +                                      mw.loader.store.set( module, registry[ module ] );
                                        for ( m in registry ) {
 -                                              if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
 +                                              if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
                                                        execute( m );
                                                }
                                        }
                                        throw new Error( 'Unknown dependency: ' + module );
                                }
  
 -                              if ( registry[module].skip !== null ) {
 +                              if ( registry[ module ].skip !== null ) {
                                        /*jshint evil:true */
 -                                      skip = new Function( registry[module].skip );
 -                                      registry[module].skip = null;
 +                                      skip = new Function( registry[ module ].skip );
 +                                      registry[ module ].skip = null;
                                        if ( skip() ) {
 -                                              registry[module].skipped = true;
 -                                              registry[module].dependencies = [];
 -                                              registry[module].state = 'ready';
 +                                              registry[ module ].skipped = true;
 +                                              registry[ module ].dependencies = [];
 +                                              registry[ module ].state = 'ready';
                                                handlePending( module );
                                                return;
                                        }
                                }
  
                                // Resolves dynamic loader function and replaces it with its own results
 -                              if ( $.isFunction( registry[module].dependencies ) ) {
 -                                      registry[module].dependencies = registry[module].dependencies();
 +                              if ( $.isFunction( registry[ module ].dependencies ) ) {
 +                                      registry[ module ].dependencies = registry[ module ].dependencies();
                                        // Ensures the module's dependencies are always in an array
 -                                      if ( typeof registry[module].dependencies !== 'object' ) {
 -                                              registry[module].dependencies = [registry[module].dependencies];
 +                                      if ( typeof registry[ module ].dependencies !== 'object' ) {
 +                                              registry[ module ].dependencies = [ registry[ module ].dependencies ];
                                        }
                                }
                                if ( $.inArray( module, resolved ) !== -1 ) {
                                        unresolved = {};
                                }
                                // Tracks down dependencies
 -                              deps = registry[module].dependencies;
 +                              deps = registry[ module ].dependencies;
                                len = deps.length;
                                for ( n = 0; n < len; n += 1 ) {
 -                                      if ( $.inArray( deps[n], resolved ) === -1 ) {
 -                                              if ( unresolved[deps[n]] ) {
 +                                      if ( $.inArray( deps[ n ], resolved ) === -1 ) {
 +                                              if ( unresolved[ deps[ n ] ] ) {
                                                        throw new Error(
                                                                'Circular reference detected: ' + module +
 -                                                              ' -> ' + deps[n]
 +                                                              ' -> ' + deps[ n ]
                                                        );
                                                }
  
                                                // Add to unresolved
 -                                              unresolved[module] = true;
 -                                              sortDependencies( deps[n], resolved, unresolved );
 -                                              delete unresolved[module];
 +                                              unresolved[ module ] = true;
 +                                              sortDependencies( deps[ n ], resolved, unresolved );
 +                                              delete unresolved[ module ];
                                        }
                                }
 -                              resolved[resolved.length] = module;
 +                              resolved[ resolved.length ] = module;
                        }
  
                        /**
                         *
                         * @private
                         * @param {string} src URL to script, will be used as the src attribute in the script tag
 -                       * @param {Function} [callback] Callback which will be run when the script is done
 +                       * @return {jQuery.Promise}
                         */
 -                      function addScript( src, callback ) {
 -                              $.ajax( {
 +                      function addScript( src ) {
 +                              return $.ajax( {
                                        url: src,
                                        dataType: 'script',
                                        // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
                                        // text, so we'd need to $.globalEval, which then messes up line numbers.
                                        crossDomain: true,
                                        cache: true
 -                              } ).always( callback );
 +                              } );
 +                      }
 +
 +                      /**
 +                       * Utility function for execute()
 +                       *
 +                       * @ignore
 +                       */
 +                      function addLink( media, url ) {
 +                              var el = document.createElement( 'link' );
 +                              // Support: IE
 +                              // Insert in document *before* setting href
 +                              getMarker().before( el );
 +                              el.rel = 'stylesheet';
 +                              if ( media && media !== 'all' ) {
 +                                      el.media = media;
 +                              }
 +                              // If you end up here from an IE exception "SCRIPT: Invalid property value.",
 +                              // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
 +                              el.href = url;
                        }
  
                        /**
                                if ( !hasOwn.call( registry, module ) ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
                                }
 -                              if ( registry[module].state === 'registered' ) {
 -                                      throw new Error( 'Module has not been requested from the server yet: ' + module );
 -                              }
 -                              if ( registry[module].state === 'loading' ) {
 -                                      throw new Error( 'Module has not completed loading yet: ' + module );
 -                              }
 -                              if ( registry[module].state === 'ready' ) {
 -                                      throw new Error( 'Module has already been executed: ' + module );
 +                              if ( registry[ module ].state !== 'loaded' ) {
 +                                      throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
                                }
  
 -                              /**
 -                               * Define loop-function here for efficiency
 -                               * and to avoid re-using badly scoped variables.
 -                               * @ignore
 -                               */
 -                              function addLink( media, url ) {
 -                                      var el = document.createElement( 'link' );
 -                                      // Support: IE
 -                                      // Insert in document *before* setting href
 -                                      getMarker().before( el );
 -                                      el.rel = 'stylesheet';
 -                                      if ( media && media !== 'all' ) {
 -                                              el.media = media;
 -                                      }
 -                                      // If you end up here from an IE exception "SCRIPT: Invalid property value.",
 -                                      // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
 -                                      el.href = url;
 -                              }
 +                              registry[ module ].state = 'executing';
  
                                runScript = function () {
                                        var script, markModuleReady, nestedAddScript, legacyWait,
                                                // and their dependencies from the legacyWait (to prevent a circular dependency).
                                                legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
                                        try {
 -                                              script = registry[module].script;
 +                                              script = registry[ module ].script;
                                                markModuleReady = function () {
 -                                                      registry[module].state = 'ready';
 +                                                      registry[ module ].state = 'ready';
                                                        handlePending( module );
                                                };
                                                nestedAddScript = function ( arr, callback, i ) {
                                                                return;
                                                        }
  
 -                                                      addScript( arr[i], function () {
 +                                                      addScript( arr[ i ] ).always( function () {
                                                                nestedAddScript( arr, callback, i + 1 );
                                                        } );
                                                };
                                        } catch ( e ) {
                                                // This needs to NOT use mw.log because these errors are common in production mode
                                                // and not in debug mode, such as when a symbol that should be global isn't exported
 -                                              registry[module].state = 'error';
 +                                              registry[ module ].state = 'error';
                                                mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
                                                handlePending( module );
                                        }
                                };
  
 -                              // This used to be inside runScript, but since that is now fired asychronously
 -                              // (after CSS is loaded) we need to set it here right away. It is crucial that
 -                              // when execute() is called this is set synchronously, otherwise modules will get
 -                              // executed multiple times as the registry will state that it isn't loading yet.
 -                              registry[module].state = 'loading';
 -
                                // Add localizations to message system
 -                              if ( registry[module].messages ) {
 -                                      mw.messages.set( registry[module].messages );
 +                              if ( registry[ module ].messages ) {
 +                                      mw.messages.set( registry[ module ].messages );
                                }
  
                                // Initialise templates
 -                              if ( registry[module].templates ) {
 -                                      mw.templates.set( module, registry[module].templates );
 +                              if ( registry[ module ].templates ) {
 +                                      mw.templates.set( module, registry[ module ].templates );
                                }
  
                                // Make sure we don't run the scripts until all stylesheet insertions have completed.
                                // * back-compat: { <media>: [url, ..] }
                                // * { "css": [css, ..] }
                                // * { "url": { <media>: [url, ..] } }
 -                              if ( registry[module].style ) {
 -                                      for ( key in registry[module].style ) {
 -                                              value = registry[module].style[key];
 +                              if ( registry[ module ].style ) {
 +                                      for ( key in registry[ module ].style ) {
 +                                              value = registry[ module ].style[ key ];
                                                media = undefined;
  
                                                if ( key !== 'url' && key !== 'css' ) {
                                                        for ( i = 0; i < value.length; i += 1 ) {
                                                                if ( key === 'bc-url' ) {
                                                                        // back-compat: { <media>: [url, ..] }
 -                                                                      addLink( media, value[i] );
 +                                                                      addLink( media, value[ i ] );
                                                                } else if ( key === 'css' ) {
                                                                        // { "css": [css, ..] }
 -                                                                      addEmbeddedCSS( value[i], cssHandle() );
 +                                                                      addEmbeddedCSS( value[ i ], cssHandle() );
                                                                }
                                                        }
                                                // Not an array, but a regular object
                                                } else if ( typeof value === 'object' ) {
                                                        // { "url": { <media>: [url, ..] } }
                                                        for ( media in value ) {
 -                                                              urls = value[media];
 +                                                              urls = value[ media ];
                                                                for ( i = 0; i < urls.length; i += 1 ) {
 -                                                                      addLink( media, urls[i] );
 +                                                                      addLink( media, urls[ i ] );
                                                                }
                                                        }
                                                }
                        function request( dependencies, ready, error ) {
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
 -                                      dependencies = [dependencies];
 +                                      dependencies = [ dependencies ];
                                }
  
                                // Add ready and error callbacks if they were given
                                if ( ready !== undefined || error !== undefined ) {
 -                                      jobs[jobs.length] = {
 +                                      jobs[ jobs.length ] = {
                                                dependencies: $.grep( dependencies, function ( module ) {
                                                        var state = mw.loader.getState( module );
                                                        return state === 'registered' || state === 'loaded' || state === 'loading';
                                        if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
                                                // Private modules must be embedded in the page. Don't bother queuing
                                                // these as the server will deny them anyway (T101806).
 -                                              if ( registry[module].group === 'private' ) {
 -                                                      registry[module].state = 'error';
 +                                              if ( registry[ module ].group === 'private' ) {
 +                                                      registry[ module ].state = 'error';
                                                        handlePending( module );
                                                        return;
                                                }
                                }
                                a.sort();
                                for ( key = 0; key < a.length; key += 1 ) {
 -                                      sorted[a[key]] = o[a[key]];
 +                                      sorted[ a[ key ] ] = o[ a[ key ] ];
                                }
                                return sorted;
                        }
                        /**
                         * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
                         * to a query string of the form foo.bar,baz|bar.baz,quux
 +                       *
                         * @private
                         */
                        function buildModulesString( moduleMap ) {
  
                                for ( prefix in moduleMap ) {
                                        p = prefix === '' ? '' : prefix + '.';
 -                                      arr.push( p + moduleMap[prefix].join( ',' ) );
 +                                      arr.push( p + moduleMap[ prefix ].join( ',' ) );
                                }
                                return arr.join( '|' );
                        }
  
                        /**
                         * Load modules from load.php
 +                       *
                         * @private
                         * @param {Object} moduleMap Module map, see #buildModulesString
                         * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
                         */
                        function resolveIndexedDependencies( modules ) {
                                $.each( modules, function ( idx, module ) {
 -                                      if ( module[2] ) {
 -                                              module[2] = $.map( module[2], function ( dep ) {
 -                                                      return typeof dep === 'number' ? modules[dep][0] : dep;
 +                                      if ( module[ 2 ] ) {
 +                                              module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
 +                                                      return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
                                                } );
                                        }
                                } );
                                        // 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 ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
 +                                              if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
                                                        // Prevent duplicate entries
 -                                                      if ( $.inArray( queue[q], batch ) === -1 ) {
 -                                                              batch[batch.length] = queue[q];
 +                                                      if ( $.inArray( queue[ q ], batch ) === -1 ) {
 +                                                              batch[ batch.length ] = queue[ q ];
                                                                // Mark registered modules as loading
 -                                                              registry[queue[q]].state = 'loading';
 +                                                              registry[ queue[ q ] ].state = 'loading';
                                                        }
                                                }
                                        }
                                                        // the error) instead of all of them.
                                                        mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
                                                        origBatch = $.grep( origBatch, function ( module ) {
 -                                                              return registry[module].state === 'loading';
 +                                                              return registry[ module ].state === 'loading';
                                                        } );
                                                        batch = batch.concat( origBatch );
                                                }
  
                                        // 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;
 +                                              bSource = registry[ batch[ b ] ].source;
 +                                              bGroup = registry[ batch[ b ] ].group;
                                                if ( !hasOwn.call( splits, bSource ) ) {
 -                                                      splits[bSource] = {};
 +                                                      splits[ bSource ] = {};
                                                }
 -                                              if ( !hasOwn.call( splits[bSource], bGroup ) ) {
 -                                                      splits[bSource][bGroup] = [];
 +                                              if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
 +                                                      splits[ bSource ][ bGroup ] = [];
                                                }
 -                                              bSourceGroup = splits[bSource][bGroup];
 -                                              bSourceGroup[bSourceGroup.length] = batch[b];
 +                                              bSourceGroup = splits[ bSource ][ bGroup ];
 +                                              bSourceGroup[ bSourceGroup.length ] = batch[ b ];
                                        }
  
                                        // Clear the batch - this MUST happen before we append any
  
                                        for ( source in splits ) {
  
 -                                              sourceLoadScript = sources[source];
 +                                              sourceLoadScript = sources[ source ];
  
 -                                              for ( group in splits[source] ) {
 +                                              for ( group in splits[ source ] ) {
  
                                                        // Cache access to currently selected list of
                                                        // modules for this group from this source.
 -                                                      modules = splits[source][group];
 +                                                      modules = splits[ source ][ group ];
  
                                                        currReqBase = $.extend( {
                                                                version: getCombinedVersion( modules )
  
                                                        for ( i = 0; i < modules.length; i += 1 ) {
                                                                // Determine how many bytes this module would add to the query string
 -                                                              lastDotIndex = modules[i].lastIndexOf( '.' );
 +                                                              lastDotIndex = modules[ i ].lastIndexOf( '.' );
  
                                                                // If lastDotIndex is -1, substr() returns an empty string
 -                                                              prefix = modules[i].substr( 0, lastDotIndex );
 -                                                              suffix = modules[i].slice( lastDotIndex + 1 );
 +                                                              prefix = modules[ i ].substr( 0, lastDotIndex );
 +                                                              suffix = modules[ i ].slice( lastDotIndex + 1 );
  
                                                                bytesAdded = hasOwn.call( moduleMap, prefix )
                                                                        ? suffix.length + 3 // '%2C'.length == 3
 -                                                                      : modules[i].length + 3; // '%7C'.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
                                                                        mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                                }
                                                                if ( !hasOwn.call( moduleMap, prefix ) ) {
 -                                                                      moduleMap[prefix] = [];
 +                                                                      moduleMap[ prefix ] = [];
                                                                }
 -                                                              moduleMap[prefix].push( suffix );
 +                                                              moduleMap[ prefix ].push( suffix );
                                                                l += bytesAdded;
                                                        }
                                                        // If there's anything left in moduleMap, request that too
                                        // Allow multiple additions
                                        if ( typeof id === 'object' ) {
                                                for ( source in id ) {
 -                                                      mw.loader.addSource( source, id[source] );
 +                                                      mw.loader.addSource( source, id[ source ] );
                                                }
                                                return true;
                                        }
                                                loadUrl = loadUrl.loadScript;
                                        }
  
 -                                      sources[id] = loadUrl;
 +                                      sources[ id ] = loadUrl;
  
                                        return true;
                                },
                                                resolveIndexedDependencies( module );
                                                for ( i = 0, len = module.length; i < len; i++ ) {
                                                        // module is an array of module names
 -                                                      if ( typeof module[i] === 'string' ) {
 -                                                              mw.loader.register( module[i] );
 +                                                      if ( typeof module[ i ] === 'string' ) {
 +                                                              mw.loader.register( module[ i ] );
                                                        // module is an array of arrays
 -                                                      } else if ( typeof module[i] === 'object' ) {
 -                                                              mw.loader.register.apply( mw.loader, module[i] );
 +                                                      } else if ( typeof module[ i ] === 'object' ) {
 +                                                              mw.loader.register.apply( mw.loader, module[ i ] );
                                                        }
                                                }
                                                return;
                                                throw new Error( 'module already registered: ' + module );
                                        }
                                        // List the module as registered
 -                                      registry[module] = {
 +                                      registry[ module ] = {
                                                version: version !== undefined ? String( version ) : '',
                                                dependencies: [],
                                                group: typeof group === 'string' ? group : null,
                                        };
                                        if ( typeof dependencies === 'string' ) {
                                                // Allow dependencies to be given as a single module name
 -                                              registry[module].dependencies = [ dependencies ];
 +                                              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;
 +                                              registry[ module ].dependencies = dependencies;
                                        }
                                },
  
                                                mw.loader.register( module );
                                        }
                                        // Check for duplicate implementation
 -                                      if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
 +                                      if ( hasOwn.call( registry, module ) && registry[ module ].script !== undefined ) {
                                                throw new Error( 'module already implemented: ' + module );
                                        }
                                        // Attach components
 -                                      registry[module].script = script || [];
 -                                      registry[module].style = style || {};
 -                                      registry[module].messages = messages || {};
 -                                      registry[module].templates = templates || {};
 +                                      registry[ module ].script = script || [];
 +                                      registry[ module ].style = style || {};
 +                                      registry[ module ].messages = messages || {};
 +                                      registry[ module ].templates = templates || {};
                                        // The module may already have been marked as erroneous
 -                                      if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
 -                                              registry[module].state = 'loaded';
 -                                              if ( allReady( registry[module].dependencies ) ) {
 +                                      if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) {
 +                                              registry[ module ].state = 'loaded';
 +                                              if ( allReady( registry[ module ].dependencies ) ) {
                                                        execute( module );
                                                }
                                        }
  
                                        if ( typeof module === 'object' ) {
                                                for ( m in module ) {
 -                                                      mw.loader.state( m, module[m] );
 +                                                      mw.loader.state( m, module[ m ] );
                                                }
                                                return;
                                        }
                                        if ( !hasOwn.call( registry, module ) ) {
                                                mw.loader.register( module );
                                        }
 -                                      if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
 -                                              && registry[module].state !== state ) {
 +                                      if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
 +                                              && registry[ module ].state !== state ) {
                                                // Make sure pending modules depending on this one get executed if their
                                                // dependencies are now fulfilled!
 -                                              registry[module].state = state;
 +                                              registry[ module ].state = state;
                                                handlePending( module );
                                        } else {
 -                                              registry[module].state = state;
 +                                              registry[ module ].state = state;
                                        }
                                },
  
                                 *  in the registry.
                                 */
                                getVersion: function ( module ) {
 -                                      if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
 +                                      if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) {
                                                return null;
                                        }
 -                                      return registry[module].version;
 +                                      return registry[ module ].version;
                                },
  
                                /**
                                 *  in the registry.
                                 */
                                getState: function ( module ) {
 -                                      if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
 +                                      if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) {
                                                return null;
                                        }
 -                                      return registry[module].state;
 +                                      return registry[ module ].state;
                                },
  
                                /**
  
                                        /**
                                         * Construct a JSON-serializable object representing the content of the store.
 +                                       *
                                         * @return {Object} Module store contents.
                                         */
                                        toJSON: function () {
  
                                        /**
                                         * Get a key on which to vary the module cache.
 +                                       *
                                         * @return {string} String of concatenated vary conditions.
                                         */
                                        getVary: function () {
                                         */
                                        getModuleKey: function ( module ) {
                                                return hasOwn.call( registry, module ) ?
 -                                                      ( module + '@' + registry[module].version ) : null;
 +                                                      ( module + '@' + registry[ module ].version ) : null;
                                        },
  
                                        /**
                                                key = mw.loader.store.getModuleKey( module );
                                                if ( key in mw.loader.store.items ) {
                                                        mw.loader.store.stats.hits++;
 -                                                      return mw.loader.store.items[key];
 +                                                      return mw.loader.store.items[ key ];
                                                }
                                                mw.loader.store.stats.misses++;
                                                return false;
                                                        ];
                                                        // Attempted workaround for a possible Opera bug (bug T59567).
                                                        // This regex should never match under sane conditions.
 -                                                      if ( /^\s*\(/.test( args[1] ) ) {
 -                                                              args[1] = 'function' + args[1];
 +                                                      if ( /^\s*\(/.test( args[ 1 ] ) ) {
 +                                                              args[ 1 ] = 'function' + args[ 1 ];
                                                                mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
                                                        }
                                                } catch ( e ) {
                                                if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
                                                        return false;
                                                }
 -                                              mw.loader.store.items[key] = src;
 +                                              mw.loader.store.items[ key ] = src;
                                                mw.loader.store.update();
                                        },
  
                                                        module = key.slice( 0, key.indexOf( '@' ) );
                                                        if ( mw.loader.store.getModuleKey( module ) !== key ) {
                                                                mw.loader.store.stats.expired++;
 -                                                              delete mw.loader.store.items[key];
 -                                                      } else if ( mw.loader.store.items[key].length > mw.loader.store.MODULE_SIZE_MAX ) {
 +                                                              delete mw.loader.store.items[ key ];
 +                                                      } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
                                                                // This value predates the enforcement of a size limit on cached modules.
 -                                                              delete mw.loader.store.items[key];
 +                                                              delete mw.loader.store.items[ key ];
                                                        }
                                                }
                                        },
                                        var v, attrName, s = '<' + name;
  
                                        for ( attrName in attrs ) {
 -                                              v = attrs[attrName];
 +                                              v = attrs[ attrName ];
                                                // Convert name=true, to name=name
                                                if ( v === true ) {
                                                        v = attrName;
  
                                /**
                                 * Wrapper object for raw HTML passed to mw.html.element().
 +                               *
                                 * @class mw.html.Raw
                                 */
                                Raw: function ( value ) {
  
                                /**
                                 * Wrapper object for CDATA element contents passed to mw.html.element()
 +                               *
                                 * @class mw.html.Cdata
                                 */
                                Cdata: function ( value ) {
                         */
                        return function ( name ) {
                                var list = hasOwn.call( lists, name ) ?
 -                                      lists[name] :
 -                                      lists[name] = $.Callbacks( 'memory' );
 +                                      lists[ name ] :
 +                                      lists[ name ] = $.Callbacks( 'memory' );
  
                                return {
                                        /**
                                         * Register a hook handler
 +                                       *
                                         * @param {Function...} handler Function to bind.
                                         * @chainable
                                         */
  
                                        /**
                                         * Unregister a hook handler
 +                                       *
                                         * @param {Function...} handler Function to unbind.
                                         * @chainable
                                         */
  
                                        /**
                                         * Run a hook.
 +                                       *
                                         * @param {Mixed...} data
                                         * @chainable
                                         */
                }
        }
  
-       // subscribe to error streams
+       // Subscribe to error streams
        mw.trackSubscribe( 'resourceloader.exception', log );
        mw.trackSubscribe( 'resourceloader.assert', log );
  
+       /**
+        * Fired when all modules associated with the page have finished loading.
+        *
+        * @event resourceloader_loadEnd
+        * @member mw.hook
+        */
+       $( function () {
+               var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
+                       return mw.loader.getState( module ) === 'loading';
+               } );
+               // In order to use jQuery.when (which stops early if one of the promises got rejected)
+               // cast any loading failures into successes. We only need a callback, not the module.
+               loading = $.map( loading, function ( module ) {
+                       return mw.loader.using( module ).then( null, function () {
+                               return $.Deferred().resolve();
+                       } );
+               } );
+               $.when.apply( $, loading ).then( function () {
+                       performance.mark( 'mwLoadEnd' );
+                       mw.hook( 'resourceloader.loadEnd' ).fire();
+               } );
+       } );
        // Attach to window and globally alias
        window.mw = window.mediaWiki = mw;
  }( jQuery ) );